1 2 3<!DOCTYPE html> 4<html lang="id"> 5<head> 6 <meta charset="UTF-8"> 7 <title>EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap</title> 8 <meta name="description" content="Rasakan sensasi relaksasi premium bersama EDUTOTO melalui akses yang cepat dan fitur yang lengkap. Dengan tampilan yang modern, setiap aktivitas bermain menjadi lebih nyaman dan menyenangkan."> 9 <meta name="robots" content="index,follow,nosnippet"> 10 <link rel="canonical" href="https://norita.mx/"> 11 <link rel="amphtml" href="https://relaksasi.norita.mx/"> 12 <link rel="alternate" href="https://relaksasi.norita.mx/" hreflang="id"> 13 <link rel="alternate" href="https://relaksasi.norita.mx/" hreflang="en"> 14 <link rel="alternate" href="https://relaksasi.norita.mx/" hreflang="x-default"> 15 <link rel="alternate" hreflang="id-ID" href="https://relaksasi.norita.mx/" /> 16 <link rel="alternate" hreflang="en-US" href="https://relaksasi.norita.mx/" /> 17 <link rel="alternate" hreflang="en-SG" href="https://relaksasi.norita.mx/" /> 18 <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
vendor: 67 bytes, lines 18-19
18 19 <script async src="https://www.googletagmanager.com/gtag/js?id=
19G-0LSM1YJSPZ
vendor: 48 bytes, lines 19-20
19" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script> 20
20<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>
20 21 <meta property="og:locale" content="id_ID"> 22 <meta property="og:site_name" content="EDUTOTO"> 23 <meta property="og:type" content="product"> 24 <meta property="og:title" content="EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap"> 25 <meta property="og:description" content="Rasakan sensasi relaksasi premium bersama EDUTOTO melalui akses yang cepat dan fitur yang lengkap. Dengan tampilan yang modern, setiap aktivitas bermain menjadi lebih nyaman dan menyenangkan."> 26 <meta property="og:url" content="https://norita.mx/"> 27 <meta property="og:image" content="img/banner-edutoto.webp"> 28 <meta property="og:image:alt" content="EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap"> 29<meta property="product:price:amount" content="10000"> 30<meta property="product:price:currency" content="IDR"> 31<meta property="product:availability" content="out of stock"> 32<meta name="twitter:card" content="summary_large_image"> 33<meta name="twitter:title" content="EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap"> 34<meta name="twitter:description" content="Rasakan sensasi relaksasi premium bersama EDUTOTO melalui akses yang cepat dan fitur yang lengkap. Dengan tampilan yang modern, setiap aktivitas bermain menjadi lebih nyaman dan menyenangkan."> 35<meta name="twitter:image" content="img/banner-edutoto.webp"> 36
37<script type="application/ld+json"> 38 { 39 "@context": "http:\/\/schema.org", 40 "@type": "BreadcrumbList", 41 "itemListElement": [ 42 { 43 "@type": "ListItem", 44 "item": { 45 "@id": "https://norita.mx/", 46 "name": "EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap" 47 }, 48 "position": 1 49 } 50 ] 51 } 52 </script>
52 53
53<script type="application/ld+json"> 54 { 55 "@context": "http:\/\/schema.org", 56 "@type": "Product", 57 "name": "EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap", 58 "description": "Rasakan sensasi relaksasi premium bersama EDUTOTO melalui akses yang cepat dan fitur yang lengkap. Dengan tampilan yang modern, setiap aktivitas bermain menjadi lebih nyaman dan menyenangkan.", 59 "image": "img/banner-edutoto.webp", 60 "offers": { 61 "@type": "http:\/\/schema.org\/Offer", 62 "price": 9999, 63 "url": "https://norita.mx/", 64 "priceCurrency": "IDR", 65 "availability": "http:\/\/schema.org\/InStock" 66 } 67 } 68 </script>
68 69
69<script type="application/ld+json"> 70 { 71 "@context": "http:\/\/schema.org\/", 72 "@type": "WebPage", 73 "speakable": { 74 "@type": "SpeakableSpecification", 75 "cssSelector": [".description"], 76 "xpath": ["\/html\/head\/title"] 77 } 78 } 79 </script>
79 80 <!-- ZS --> 81 <!-- Schema JSON untuk SBO -->
82<script type="application/ld+json"> 83{ 84"@context": "https://schema.org/", 85"@type": "Product", 86"name": "EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap", 87"url": "https://norita.mx/", 88"sku": "EDUTOTO", 89"image": [ 90 "img/banner-edutoto.webp" 91], 92"description": "Rasakan sensasi relaksasi premium bersama EDUTOTO melalui akses yang cepat dan fitur yang lengkap. Dengan tampilan yang modern, setiap aktivitas bermain menjadi lebih nyaman dan menyenangkan.", 93"brand": { 94 "@type": "Organization", 95 "name": "EDUTOTO LOGIN" 96}, 97"offers": { 98 "@type": "Offer", 99 "name": "EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap", 100 "sku": "EDUTOTO LOGIN", 101 "price": "9999", 102 "priceCurrency": "IDR", 103 "availability": "https://schema.org/InStock" 104} 105} 106</script>
106 107
108<script type="application/ld+json"> 109{ 110"@context": "https://schema.org/", 111"@type": "Product", 112"name": "EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap", 113"image": "img/banner-edutoto.webp", 114"description": "Rasakan sensasi relaksasi premium bersama EDUTOTO melalui akses yang cepat dan fitur yang lengkap. Dengan tampilan yang modern, setiap aktivitas bermain menjadi lebih nyaman dan menyenangkan.", 115"sku": "EDUTOTO LOGIN", 116"offers": { 117 "@type": "Offer", 118 "priceCurrency": "IDR", 119 "url": "https://norita.mx/", 120 "availability": "https://schema.org/InStock", 121 "price": "9999.00" 122} 123} 124</script>
124 125
126<script type="application/ld+json" id="bv-jsonld-bvloader-summary"> 127{ 128"@context": "https://schema.org/", 129"@id": "https://norita.mx/", 130"aggregateRating": { 131 "@type": "AggregateRating", 132 "itemReviewed": { 133 "@type": "IndividualProduct", 134 "name": "EDUTOTO" 135 }, 136 "ratingValue": "5.0", 137 "reviewCount": 954638, 138 "bestRating": 5 139} 140} 141</script>
141 142
143<script type="application/ld+json"> 144{ 145"@context": "https://schema.org", 146"@type": "BreadcrumbList", 147"itemListElement": [ 148 { 149 "@type": "ListItem", 150 "position": 1, 151 "item": { 152 "@id": "https://norita.mx/", 153 "name": "EDUTOTO" 154 } 155 }, 156 { 157 "@type": "ListItem", 158 "position": 2, 159 "item": { 160 "@id": "https://norita.mx/", 161 "name": "EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap" 162 } 163 }, 164 { 165 "@type": "ListItem", 166 "position": 3, 167 "item": { 168 "@id": "https://norita.mx/", 169 "name": "EDUTOTO LOGIN" 170 } 171 }, 172 { 173 "@type": "ListItem", 174 "position": 4, 175 "item": { 176 "@id": "https://norita.mx/", 177 "name": "EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap" 178 } 179 } 180] 181} 182</script>
182 183
184<script type="application/ld+json" id="bv-jsonld-reviews-data"> 185{ 186 "@context": "https://schema.org", 187 "@type": "Organization", 188 "@id": "https://norita.mx/", 189 "name": "EDUTOTO", 190 "alternateName": "EDUTOTO", 191 "url": "https://norita.mx/", 192 "review": [ 193 { 194 "@type": "Review", 195 "dateCreated": "2026-03-31T12:28:35+00:00", 196 "datePublished": "2026-03-31T12:28:35+00:00", 197 "headline": "Akses Cepat Sejak Awal", 198 "reviewBody": "Sejak pertama masuk, EDUTOTO terasa nyaman. Menu-menunya rapi, aksesnya cepat, dan saya bisa langsung bermain tanpa ribet.", 199 "reviewRating": { 200 "@type": "Rating", 201 "ratingValue": 5, 202 "bestRating": 5, 203 "worstRating": 1 204 }, 205 "author": { 206 "@type": "Person", 207 "name": "Raka Putra â Bandung" 208 } 209 }, 210 { 211 "@type": "Review", 212 "dateCreated": "2026-04-02T18:28:21+00:00", 213 "datePublished": "2026-04-03T18:28:21+00:00", 214 "headline": "Relaksasi yang Terasa Nyata", 215 "reviewBody": "Tampilannya elegan dan tidak bikin pusing. Saya main di HP setiap hari, alurnya ringan, dan suasana relaksasinya benar-benar terasa.", 216 "reviewRating": { 217 "@type": "Rating", 218 "ratingValue": 5, 219 "bestRating": 5, 220 "worstRating": 1 221 }, 222 "author": { 223 "@type": "Person", 224 "name": "Nadia Kusuma â Surabaya" 225 } 226 }, 227 { 228 "@type": "Review", 229 "dateCreated": "2026-04-05T19:28:18+00:00", 230 "datePublished": "2026-04-05T19:28:18+00:00", 231 "headline": "Stabil dan Praktis", 232 "reviewBody": "Yang bikin saya betah di EDUTOTO adalah sistemnya stabil dan fiturnya lengkap. Login sampai transaksi terasa praktis, jadi mainnya lebih tenang.", 233 "reviewRating": { 234 "@type": "Rating", 235 "ratingValue": 5, 236 "bestRating": 5, 237 "worstRating": 1 238 }, 239 "author": { 240 "@type": "Person", 241 "name": "Fajar Santoso â Yogyakarta" 242 } 243 } 244 ] 245} 246</script>
vendor: 2 bytes, line 246
246
247<script type="application/ld+json"> 248{ 249 "@context": "https://schema.org", 250 "@type": "FAQPage", 251 "mainEntity": [ 252 { 253 "@type": "Question", 254 "name": "Apa yang membuat pengalaman EDUTOTO terasa lebih premium?", 255 "acceptedAnswer": { 256 "@type": "Answer", 257 "text": "EDUTOTO memadukan desain elegan, akses yang ringkas, dan fitur lengkap dalam satu platform. Kombinasi itu membuat setiap sesi terasa lebih tenang, nyaman, dan berkelas sejak pengguna pertama kali masuk." 258 } 259 }, 260 { 261 "@type": "Question", 262 "name": "Apakah pemula bisa langsung bermain di EDUTOTO?", 263 "acceptedAnswer": { 264 "@type": "Answer", 265 "text": "Bisa. Antarmuka EDUTOTO sederhana dan modern, sehingga pemula tidak perlu melalui langkah yang berbelit. Menu favorit mudah ditemukan, dan alur bermain tetap jelas bagi pemain yang sudah aktif." 266 } 267 }, 268 { 269 "@type": "Question", 270 "name": "Bagaimana EDUTOTO menjaga suasana relaksasi saat bermain?", 271 "acceptedAnswer": { 272 "@type": "Answer", 273 "text": "Nuansa relaksasi dijaga lewat tampilan yang rapi, pilihan game berkualitas, dan sistem yang selalu diperbarui. Performa yang stabil membuat sesi bermain terasa tenang tanpa gangguan yang mengganggu kenyamanan." 274 } 275 }, 276 { 277 "@type": "Question", 278 "name": "Apakah EDUTOTO bisa dibuka di HP dan komputer?", 279 "acceptedAnswer": { 280 "@type": "Answer", 281 "text": "Ya. EDUTOTO mendukung akses di smartphone maupun komputer. Tampilan responsif, navigasi yang intuitif, dan proses transaksi yang ringan membuat login hingga bermain tetap praktis di perangkat mana pun." 282 } 283 }, 284 { 285 "@type": "Question", 286 "name": "Mengapa EDUTOTO cocok dijadikan pilihan utama?", 287 "acceptedAnswer": { 288 "@type": "Answer", 289 "text": "EDUTOTO cocok karena menawarkan akses cepat, fitur premium, dan reputasi positif. Bagi siapa pun yang mencari platform bermain yang lebih santai dan terpercaya, pengalaman di sini terasa lebih nyaman sekaligus meyakinkan." 290 } 291 } 292 ] 293} 294</script>
294 295 <!-- User Session Data --> 296 <meta name="csrf-token" content="9ce6e4a17f7cc189893df9e47789b13e98829450d516a0cae5ca7b5f43e5dadc"> 297 <meta name="user-logged-in" content="false"> 298 <meta name="user-id" content="0"> 299 300 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> 301 302 <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 303 <style data-inlined="style.css"> 304@font-face { 305 font-family: 'Segoe UI'; 306 src: url('/assets/fonts/segoeui.woff2') format('woff2'), 307 url('/assets/fonts/segoeui.woff') format('woff'); 308 font-weight: 400; 309 font-style: normal; 310} 311 312:root { 313 --brand-50:#FFFBEB; 314 --brand-100:#FEF3C7; 315 --brand-500:#C9A227; 316 --brand-600:#8B6914; 317 --gray-900:#1F1F23; 318 --gray-700:#3D3F45; 319 --gray-500:#6F737B; 320 --gray-300:#C5C9CF; 321 --gray-100:#F3F5F7; 322 --border:#E4E7EB; 323 --radius:8px; 324 --transition: .18s ease; 325 /* font-family: "Inter", system-ui, sans-serif; */ 326 font-family: 'Segoe UI', sans-serif; 327} 328 329* { box-sizing: border-box; } 330body { 331 margin:0; 332 padding-bottom: 0; /* Reset for desktop */ 333 color:var(--gray-900); 334 background:#fff; 335} 336img { max-width:100%; display:block; } 337 338.container { 339 width:100%; 340 max-width:1280px; 341 margin:0 auto; 342 padding:10px 0; 343} 344 345.topbar { 346 background:#111; 347 color:#fff; 348 font-size:13px; 349} 350.topbar-inner { 351 display:flex; 352 justify-content:space-between; 353 align-items:center; 354 min-height:36px; 355} 356.topbar a { color:#fff; text-decoration:none; margin-left:12px; } 357 358/* ========== Desktop base (Glassmorphism Modern with Warm Gradient) ========== */ 359.navbar { 360 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,251,235,0.85) 100%); 361 backdrop-filter:blur(12px); 362 -webkit-backdrop-filter:blur(12px); 363 border-bottom:1px solid rgba(228,231,235,0.6); 364 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 365 position:sticky; 366 top:0; 367 z-index:500; 368 transition:all .3s ease; 369} 370 371/* Accent line gradient gold */ 372.navbar::after { 373 content:''; 374 position:absolute; 375 bottom:0; 376 left:0; 377 right:0; 378 height:2px; 379 background:linear-gradient(90deg,transparent 0%, var(--brand-500) 20%, var(--brand-600) 50%, var(--brand-500) 80%, transparent 100%); 380 opacity:0.5; 381} 382 383.nav-inner { 384 display:flex; 385 align-items:center; 386 gap:24px; 387 min-height:70px; 388} 389.nav-inner .search .m-search-ico { display: none; } 390 391/* Secara default (desktop) ikon login disembunyikan; teks muncul */ 392.login-btn .login-ico { display:none; } 393.navbar-logo { 394 display: none; 395 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,251,235,0.85) 100%); 396 backdrop-filter:blur(12px); 397 -webkit-backdrop-filter:blur(12px); 398 border-bottom:1px solid rgba(228,231,235,0.6); 399 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 400 position:sticky; 401 top:0; 402 z-index:500; 403} 404 405/* Hide bottom navigation on desktop - only show on mobile */ 406.bottom-nav { 407 display: none; 408} 409 410/* ============================================================== 411 MOBILE (<=640px) 412 ============================================================== */ 413@media (max-width:640px){ 414 .container { padding:10px 16px; } 415 416 .topbar { display:none !important; } 417 418 /* Add padding bottom for sticky bottom nav */ 419 body { 420 padding-bottom: 68px; /* Height of bottom nav + safe area */ 421 } 422 423 /* Mobile Top Navbar - Clean & Minimal */ 424 .navbar { 425 background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,251,235,0.9) 100%); 426 backdrop-filter:blur(16px); 427 -webkit-backdrop-filter:blur(16px); 428 box-shadow:0 2px 12px -3px rgba(0,0,0,0.08); 429 border: 0; 430 } 431 432 /* Simple grid layout - Logo, Search, Category */ 433 .nav-inner { 434 display:grid; 435 grid-template-columns:1fr; 436 gap:10px; 437 padding:12px 16px 0 12px; 438 min-height:auto; 439 } 440 441 /* Logo centered at top */ 442 .nav-inner .logo { 443 justify-self:center; 444 margin:0; 445 display:flex; 446 } 447 .nav-inner .logo img { height:36px; } 448 449 /* Search bar - full width */ 450 .nav-inner .search { 451 position:relative; 452 display:flex;
453 align-items:center; 454 gap:0; 455 width:100%; 456 height:44px; 457 background:#fff; 458 border:1.5px solid #e5e7eb; 459 border-radius:12px; 460 padding:0 12px; 461 box-shadow:0 1px 3px rgba(0,0,0,0.04); 462 transition:all .25s; 463 } 464 .nav-inner .search:focus-within { 465 border-color:#FBBF24; 466 box-shadow:0 0 0 3px rgba(251, 191, 36, 0.1); 467 } 468 469 .nav-inner .search-icon-btn { 470 color:#9ca3af; 471 padding:4px; 472 margin-right:4px; 473 } 474 475 .nav-inner .search-icon-btn svg { 476 width:18px; 477 height:18px; 478 } 479 480 .nav-inner .search input { 481 flex:1; 482 min-width:0; 483 border:none; 484 outline:none; 485 background:transparent; 486 font-size:14px; 487 padding:0 8px; 488 height:100%; 489 } 490 491 .nav-inner .ai-bot-toggle { 492 padding:6px; 493 margin-left:0; 494 } 495 496 .nav-inner .ai-bot-toggle .bot-icon { 497 width:20px; 498 height:20px; 499 } 500 501 .nav-inner .search-suggest { 502 left:0; 503 right:0; 504 width:calc(100vw - 32px); 505 margin-top:6px; 506 border-radius:12px; 507 max-height:70vh; 508 } 509 510 /* Hide desktop nav actions but keep structure for userMenu */ 511 .nav-inner .nav-actions { 512 display:flex !important; /* Keep flex to allow userMenu positioning */ 513 visibility:hidden; /* Hide visually but keep in DOM */ 514 pointer-events:none; /* Disable interactions with hidden elements */ 515 position:absolute; /* Remove from flow */ 516 right:0; 517 top:0; 518 } 519 520 /* Re-enable userMenu for logged-in users */ 521 .nav-inner .nav-actions #userMenu { 522 visibility:visible !important; 523 pointer-events:auto !important; 524 } 525 526 .nav-inner .logo { display: none; } 527 .navbar-logo { 528 display: block; 529 /* background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,251,235,0.9) 100%); */ 530 backdrop-filter:blur(16px); 531 -webkit-backdrop-filter:blur(16px); 532 position: static; /* Remove sticky on mobile */ 533 box-shadow: none; 534 border-bottom: 0; 535 padding-top: .6rem; 536 } 537 .nav-inner .logo-mobile { display: block; } 538 539 /* Category menu wrapper will handle visibility */ 540 541 /* ============================================================== 542 BOTTOM NAVIGATION - Styles moved to bottom-nav.css 543 ============================================================== */ 544 /* All bottom-nav styles now in /assets/css/bottom-nav.css */ 545 546 /* Hide desktop elements that now moved to bottom nav */ 547 #wishlistBtn, 548 #notifBtn, 549 #loginBtn { display:none !important; } 550 551 /* Hide desktop user menu on mobile - use bottom sheet instead */ 552 .navbar #userMenu { 553 display:none !important; 554 } 555 556 .nav-actions .cart-indicator, 557 .nav-actions .compare-indicator { 558 display:none !important; 559 } 560 561 /* Hide category button from bottom nav */ 562 #bottomCategoryBtn { 563 display: none !important; 564 } 565 566 /* User menu button styling for mobile - scoped to navbar only */ 567 .navbar #userMenu .login-btn.logged-in { 568 width:40px; 569 height:40px; 570 padding:0; 571 border-radius:50%; 572 display:flex;
573 align-items:center; 574 justify-content:center; 575 background:rgba(255,255,255,0.95); 576 backdrop-filter:blur(12px); 577 -webkit-backdrop-filter:blur(12px); 578 border:1px solid rgba(228,231,235,0.6); 579 box-shadow:0 2px 12px -3px rgba(0,0,0,0.12); 580 } 581 582 .navbar #userMenu .login-btn.logged-in .login-text { 583 display:none; 584 } 585 586 .navbar #userMenu .login-btn.logged-in .login-ico { 587 display:inline; 588 font-size:20px; 589 line-height:1; 590 } 591 592 /* User dropdown positioning for mobile */ 593 .navbar #userMenu .user-dropdown { 594 position:fixed !important; 595 top:60px !important; 596 right:16px !important; 597 left:auto !important; 598 min-width:200px; 599 background:rgba(255,255,255,0.98); 600 backdrop-filter:blur(20px); 601 -webkit-backdrop-filter:blur(20px); 602 border:1px solid rgba(228,231,235,0.6); 603 box-shadow:0 8px 24px -8px rgba(0,0,0,0.2); 604 z-index:1002 !important; 605 border-radius:12px; 606 padding:8px 0; 607 } 608 609 /* Force visibility when not hidden attribute */ 610 .navbar #userMenu .user-dropdown:not([hidden]) { 611 display:block !important; 612 visibility:visible !important; 613 opacity:1 !important; 614 } 615 616 /* Container untuk kategori dan tombol kategori sejajar */ 617 .navbar .container:last-child { 618 display:flex !important; 619 align-items:center !important; 620 padding:4px 16px 6px !important; 621 gap:5px; 622 overflow:visible; 623 } 624 625 /* Wrapper untuk mega-cats dengan fade gradient (mobile only) */ 626 .mega-cats-wrapper { 627 position:relative; 628 flex:1; 629 overflow:hidden; 630 min-height:34px; /* Ensure content is visible */ 631 display:flex; /* Ensure wrapper is flex container */ 632 } 633 634 /* Fade gradient indicator di ujung kanan (mobile only) */ 635 .mega-cats-fade { 636 position:absolute; 637 top:0; 638 right:0; 639 bottom:0; 640 width:80px; /* Increased from 60px for better visibility */ 641 background:linear-gradient(to left, 642 rgba(255,251,235,1) 0%, 643 rgba(255,251,235,0.98) 15%, 644 rgba(255,251,235,0.85) 35%, 645 transparent 100%); 646 pointer-events:none; 647 z-index:5; /* Increased z-index */ 648 transition:opacity 0.3s ease; 649 } 650 651 /* Kategori menu di mobile */ 652 .mega-cats.container { 653 display:flex !important; 654 gap:12px; 655 overflow-x:auto; 656 overflow-y:visible; 657 -webkit-overflow-scrolling:touch; 658 scrollbar-width:none; 659 flex:1; 660 padding:0 !important; 661 margin:0 !important; 662 position:relative; /* For z-index stacking */ 663 z-index:1; /* Below fade gradient */ 664 } 665 .mega-cats.container::-webkit-scrollbar { display:none; } 666 667 /* Styling untuk menu items di mobile */ 668 .mega-cats a { 669 font-size:13px; 670 padding:6px 10px; 671 white-space:nowrap; 672 flex-shrink:0; 673 position:relative; /* Ensure proper stacking */ 674 } 675 .mega-cats a.active { 676 background:var(--brand-600); 677 color:#fff; 678 font-weight:600; 679 border-radius:6px; 680 } 681 682 /* Tombol kategori di mobile */ 683 .mm-wp { 684 flex-shrink:0; 685 } 686 .mm2-trigger { 687 display:flex;
688 align-items:center; 689 gap:6px; 690 padding:6px 12px; 691 background:var(--gray-100); 692 color:var(--gray-700); 693 text-decoration:none; 694 border-radius:6px; 695 font-size:13px; 696 white-space:nowrap; 697 } 698 699 .mm2-trigger .label-cat {display: none; } 700 /* Tombol more kategori */ 701 .mega-more-btn { 702 border:1px solid var(--border); 703 background:#fff; 704 border-radius:10px; 705 padding:6px 14px; 706 font-size:18px; 707 cursor:pointer; 708 } 709 .mega-cats-more.active > .mega-more-btn { 710 background:var(--brand-600); 711 color:#fff; 712 border-color:var(--brand-600); 713 } 714 .mega-more-dropdown { 715 top:calc(100% + 10px); 716 right:0; 717 min-width:85vw; 718 max-width:90vw; 719 border-radius:16px; 720 } 721 .mega-more-grid { 722 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 723 } 724 725 /* ============================================================== 726 MOBILE USER BOTTOM SHEET 727 ============================================================== */ 728 .mobile-user-sheet { 729 position:fixed; 730 top:0; 731 left:0; 732 right:0; 733 bottom:0; 734 z-index:10000; 735 pointer-events:none; 736 } 737 738 .mobile-user-sheet.active { 739 pointer-events:auto; 740 } 741 742 .mobile-user-sheet-backdrop { 743 position:absolute; 744 top:0; 745 left:0; 746 right:0; 747 bottom:0; 748 background:rgba(0,0,0,0.5); 749 opacity:0; 750 transition:opacity .3s ease; 751 } 752 753 .mobile-user-sheet.active .mobile-user-sheet-backdrop { 754 opacity:1; 755 } 756 757 .mobile-user-sheet-content { 758 position:absolute; 759 bottom:0; 760 left:0; 761 right:0; 762 background:#fff; 763 border-radius:20px 20px 0 0; 764 box-shadow:0 -4px 24px rgba(0,0,0,0.15); 765 transform:translateY(100%); 766 transition:transform .3s cubic-bezier(0.4, 0.0, 0.2, 1); 767 padding-bottom:calc(68px + env(safe-area-inset-bottom)); 768 } 769 770 .mobile-user-sheet.active .mobile-user-sheet-content { 771 transform:translateY(0); 772 } 773 774 .mobile-user-sheet-header { 775 padding:16px 20px 12px; 776 border-bottom:1px solid #e5e7eb; 777 } 778 779 .mobile-user-sheet-handle { 780 width:40px; 781 height:4px; 782 background:#d1d5db; 783 border-radius:2px; 784 margin:0 auto 16px; 785 } 786 787 .mobile-user-info { 788 display:flex;
789 align-items:center; 790 gap:12px; 791 } 792 793 .mobile-user-avatar { 794 width:48px; 795 height:48px; 796 border-radius:50%; 797 background:linear-gradient(135deg, #FEF3C7, #FDE68A); 798 display:flex; 799 align-items:center; 800 justify-content:center; 801 font-size:24px; 802 flex-shrink:0; 803 } 804 805 .mobile-user-details { 806 flex:1; 807 min-width:0; 808 } 809 810 .mobile-user-name { 811 font-size:16px; 812 font-weight:600; 813 color:#111827; 814 margin-bottom:2px; 815 white-space:nowrap; 816 overflow:hidden; 817 text-overflow:ellipsis; 818 } 819 820 .mobile-user-email { 821 font-size:13px; 822 color:#6b7280; 823 white-space:nowrap; 824 overflow:hidden; 825 text-overflow:ellipsis; 826 } 827 828 .mobile-user-sheet-body { 829 padding:8px 0; 830 } 831 832 .mobile-user-sheet-item { 833 display:flex; 834 align-items:center; 835 gap:16px; 836 padding:14px 20px; 837 text-decoration:none; 838 color:#374151; 839 font-size:15px; 840 transition:background .2s; 841 border:none; 842 background:none; 843 width:100%; 844 text-align:left; 845 cursor:pointer; 846 } 847 848 .mobile-user-sheet-item:active { 849 background:#f3f4f6; 850 } 851 852 .mobile-user-sheet-icon { 853 font-size:20px; 854 width:24px; 855 text-align:center; 856 flex-shrink:0; 857 } 858 859 .mobile-user-sheet-label { 860 flex:1; 861 font-weight:500; 862 } 863 864 .mobile-user-sheet-divider { 865 height:1px; 866 background:#e5e7eb; 867 margin:8px 0; 868 } 869} 870 871/* END NAVBAR */ 872 873.logo img { display:block; } 874.search { 875 flex:1; 876 position:relative; 877 display:flex; 878 align-items:center; 879 gap:0; 880 background:#fff; 881 border:1.5px solid #e5e7eb; 882 border-radius:12px; 883 padding:0 16px; 884 height:38px; 885 transition:all 0.3s ease; 886} 887.search:focus-within { 888 border-color:#FBBF24; 889 box-shadow:0 0 0 3px rgba(251, 191, 36, 0.1); 890} 891.search input { 892 flex:1; 893 border:none; 894 outline:none; 895 background:transparent; 896 font-size:15px; 897 padding:0 12px; 898 height:100%; 899 color:#1f2937; 900} 901.search input::placeholder { 902 color:#9ca3af; 903} 904.search-icon-btn { 905 background:transparent; 906 border:none; 907 padding:0; 908 display:flex; 909 align-items:center; 910 justify-content:center; 911 cursor:pointer; 912 color:#6b7280; 913 transition:color 0.2s ease; 914 flex-shrink:0; 915} 916.search-icon-btn:hover { 917 color:#FBBF24; 918} 919.search-suggest { 920 position:absolute; 921 left:0; right:0; top:100%; 922 background:#fff; 923 border:1px solid var(--border); 924 border-radius:var(--radius); 925 padding:8px 0; 926 box-shadow:0 8px 20px rgba(0,0,0,.08); 927 margin-top:4px; 928 font-size:14px; 929 max-height:260px; 930 overflow:auto; 931} 932.search-suggest div { 933 padding:6px 12px; 934 cursor:pointer; 935} 936.search-suggest div:hover { background:var(--gray-100); } 937 938.nav-actions { display:flex; gap:10px; align-items:center; } 939.icon-btn, .login-btn { 940 background:linear-gradient(135deg, rgba(255,255,255,0.95), rgba(249,250,251,0.9)); 941 border:1px solid rgba(228,231,235,0.6); 942 padding:8px 12px; 943 border-radius:var(--radius); 944 cursor:pointer; 945 font-size:14px; 946 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 947 height: 38px; 948 position:relative; 949 box-shadow: 950 0 1px 3px rgba(0,0,0,0.06), 951 0 0 0 1px rgba(255,255,255,0.5) inset; 952 backdrop-filter:blur(8px); 953 -webkit-backdrop-filter:blur(8px); 954} 955.icon-btn:hover, .login-btn:hover { 956 background:linear-gradient(135deg, rgba(249,250,251,1), rgba(243,244,246,0.95)); 957 border-color:rgba(209,213,219,0.8); 958 box-shadow: 959 0 2px 8px rgba(0,0,0,0.08), 960 0 0 0 1px rgba(255,255,255,0.6) inset; 961 transform:translateY(-1px); 962} 963.cart-indicator, .compare-indicator { position:relative; } 964.badge { 965 background:linear-gradient(135deg, #EF4444, #DC2626); 966 color:#fff; 967 font-size:10.5px; 968 font-weight:600; 969 padding:2px 6px; 970 border-radius:999px; 971 position:absolute; 972 top:-6px; 973 right:-6px; 974 min-width:18px; 975 height:18px; 976 display:flex; 977 align-items:center; 978 justify-content:center; 979 box-shadow: 980 0 2px 8px rgba(239,68,68,0.4), 981 0 0 0 2px #fff, 982 0 0 12px -2px rgba(239,68,68,0.6); 983 transition:all 0.3s ease; 984} 985.icon-btn:hover .badge { 986 transform:scale(1.08); 987 box-shadow: 988 0 4px 12px rgba(239,68,68,0.5), 989 0 0 0 2px #fff, 990 0 0 16px -2px rgba(239,68,68,0.7); 991} 992.badge.is-empty { display:none; } 993 994/* User Menu Dropdown */ 995.user-menu { 996 position:relative; 997} 998.user-dropdown { 999 position:absolute; 1000 top:calc(100% + 8px); 1001 right:0; 1002 background:#fff; 1003 border:1px solid var(--border); 1004 border-radius:12px; 1005 box-shadow:0 10px 40px rgba(0,0,0,0.12);
1006 min-width:200px; 1007 padding:8px 0; 1008 z-index:9999; 1009} 1010 1011/* Ensure dropdown is visible when not hidden */ 1012.user-dropdown:not([hidden]) { 1013 display:block !important; 1014 visibility:visible !important; 1015 opacity:1 !important; 1016} 1017 1018/* Mobile: Force user dropdown to be visible above everything */ 1019@media (max-width:640px) { 1020 .user-dropdown:not([hidden]) { 1021 position:fixed !important; 1022 z-index:9999 !important; 1023 top:60px !important; 1024 right:16px !important; 1025 } 1026} 1027 1028.dropdown-item { 1029 display:flex; 1030 align-items:center; 1031 gap:12px; 1032 padding:10px 16px; 1033 text-decoration:none; 1034 color:var(--gray-700); 1035 font-size:14px; 1036 transition:var(--transition); 1037 border:none; 1038 background:none; 1039 width:100%; 1040 text-align:left; 1041 cursor:pointer; 1042} 1043.dropdown-item:hover { 1044 background:var(--gray-100); 1045 color:var(--gray-900); 1046} 1047.dropdown-icon { 1048 font-size:16px; 1049 opacity:0.7; 1050} 1051.dropdown-separator { 1052 height:1px; 1053 background:var(--border); 1054 margin:8px 0; 1055} 1056.login-btn.logged-in { 1057 background:linear-gradient(135deg, rgba(255,251,235,0.8), rgba(219,234,254,0.6)); 1058 border:1px solid rgba(201,162,39,0.2); 1059 color:var(--brand-700); 1060 max-width:200px; /* Prevent button from getting too wide */ 1061 display:inline-flex; 1062 align-items:center; 1063 gap:10px; 1064 padding:4px 12px 4px 4px; 1065 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1066 position:relative; 1067 overflow:hidden; 1068 box-shadow: 1069 0 1px 3px rgba(201,162,39,0.08), 1070 0 0 0 1px rgba(255,255,255,0.5) inset, 1071 0 0 12px -6px rgba(201,162,39,0.1); 1072 backdrop-filter:blur(8px); 1073 -webkit-backdrop-filter:blur(8px); 1074} 1075/* Subtle background glow effect */ 1076.login-btn.logged-in::before { 1077 content:''; 1078 position:absolute; 1079 top:50%; 1080 left:0; 1081 width:32px; 1082 height:32px; 1083 background:radial-gradient(circle, rgba(201,162,39,0.15), transparent 70%); 1084 transform:translateY(-50%); 1085 opacity:0; 1086 transition:opacity 0.3s ease; 1087} 1088.login-btn.logged-in:hover::before { 1089 opacity:1; 1090} 1091.login-btn.logged-in .user-avatar { 1092 width:32px; 1093 height:32px; 1094 border-radius:50%; 1095 background:linear-gradient(135deg, #FEF3C7 0%, #FDE68A 50%, #FCD34D 100%); 1096 color:#8B6914; 1097 font-size:11.5px; 1098 font-weight:700; 1099 display:flex; 1100 align-items:center; 1101 justify-content:center; 1102 flex-shrink:0; 1103 letter-spacing:0.3px; 1104 position:relative; 1105 box-shadow: 1106 0 2px 8px rgba(201,162,39,0.25), 1107 0 1px 3px rgba(0,0,0,0.12), 1108 inset 0 1px 0 rgba(255,255,255,0.5); 1109 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1110} 1111/* Ring gradient border */ 1112.login-btn.logged-in .user-avatar::before { 1113 content:''; 1114 position:absolute; 1115 inset:-2px; 1116 border-radius:50%; 1117 padding:2px; 1118 background:linear-gradient(135deg, #C9A227, #FDE68A, #C9A227); 1119 -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1120 -webkit-mask-composite:xor; 1121 mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1122 mask-composite:exclude; 1123 opacity:0; 1124 transition:opacity 0.3s ease; 1125} 1126.login-btn.logged-in:hover .user-avatar::before { 1127 opacity:1; 1128} 1129.login-btn.logged-in:hover .user-avatar { 1130 transform:scale(1.08) rotate(-3deg); 1131 box-shadow: 1132 0 4px 12px rgba(201,162,39,0.35), 1133 0 2px 6px rgba(0,0,0,0.15), 1134 inset 0 1px 0 rgba(255,255,255,0.6); 1135} 1136.login-btn.logged-in .login-text { 1137 overflow:hidden; 1138 text-overflow:ellipsis; 1139 white-space:nowrap; 1140 flex:1; 1141 min-width:0; 1142 font-weight:500; 1143 font-size:14px; 1144 transition:color 0.2s ease; 1145} 1146.login-btn.logged-in:hover .login-text { 1147 color:var(--brand-600); 1148} 1149.login-btn.logged-in:hover { 1150 background:linear-gradient(135deg, rgba(255,251,235,0.95), rgba(219,234,254,0.85)); 1151 border-color:rgba(201,162,39,0.3); 1152 box-shadow: 1153 0 4px 16px -4px rgba(201,162,39,0.25), 1154 0 0 0 1px rgba(201,162,39,0.1) inset, 1155 0 1px 2px rgba(0,0,0,0.05); 1156 transform:translateY(-1px); 1157} 1158 1159/* Desktop: hide wrapper elements, show menu normally */ 1160@media (min-width: 761px) { 1161 .mega-cats-wrapper { 1162 display:contents; /* Remove wrapper from layout */ 1163 } 1164 .mega-cats-fade { 1165 display:none !important; /* Hide fade on desktop */ 1166 } 1167} 1168 1169.mega-cats { 1170 display:flex; 1171 gap:5px; 1172 /* padding:6px 16px 10px; */ 1173 scrollbar-width:none; 1174 position: relative; 1175 z-index: 510; 1176 overflow-x:auto; 1177 -webkit-overflow-scrolling:touch;
1178 flex:1; 1179} 1180.mega-cats::-webkit-scrollbar { 1181 display:none; 1182} 1183.mega-cats a { 1184 text-decoration:none; 1185 font-size:14px; 1186 color:var(--gray-700); 1187 padding:6px 10px; 1188 border-radius:6px; 1189 white-space:nowrap; 1190 transition:var(--transition); 1191} 1192.mega-cats a:hover { background:var(--brand-100); color:var(--brand-600); } 1193/*.mega-cats::after { 1194 content:''; 1195 position:absolute; 1196 right:0; 1197 top:0; 1198 width:40px; 1199 height:100%; 1200 background:linear-gradient(to right,rgba(255,255,255,0),#fff); 1201 pointer-events:none; 1202}*/ 1203 1204/* Tambahan untuk tombol More & dropdown. 1205 Style dasar .mega-cats dan .mega-cats a tetap pakai definisi Anda. */ 1206 1207.mega-cats-more { 1208 position:relative; 1209 display:flex; 1210 align-items:center; 1211} 1212 1213.mega-cats-more.active > .mega-more-btn { 1214 background:var(--brand-600); 1215 color:#fff; 1216} 1217 1218.mega-more-btn { 1219 cursor:pointer; 1220 border:none; 1221 background:var(--gray-100,#f5f5f5); 1222 color:var(--gray-700,#333); 1223 padding:6px 12px; 1224 font-size:16px; 1225 line-height:1; 1226 border-radius:6px; 1227 display:flex; 1228 align-items:center; 1229 justify-content:center; 1230 transition:var(--transition); 1231} 1232.mega-more-btn:hover { 1233 background:var(--brand-100); 1234 color:var(--brand-600); 1235} 1236 1237.mega-more-dropdown { 1238 position:absolute; 1239 top:100%; 1240 right:0; 1241 margin-top:8px; 1242 background:#fff; 1243 border:1px solid var(--gray-200,#e3e6e9); 1244 border-radius:10px; 1245 padding:12px 14px 14px; 1246 min-width:400px; 1247 max-width:520px; 1248 max-height:420px; 1249 box-shadow:0 12px 32px -8px rgba(25,40,65,.25); 1250 z-index: 3000; 1251} 1252 1253.mega-more-grid { 1254 display:grid; 1255 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 1256 gap:8px 10px; 1257 overflow-x: hidden; 1258 max-height:380px; 1259 overscroll-behavior:contain; 1260 scrollbar-width:thin; 1261} 1262.mega-more-grid::-webkit-scrollbar { 1263 width:6px; 1264} 1265.mega-more-grid::-webkit-scrollbar-thumb { 1266 background:var(--gray-300,#c7ccd1); 1267 border-radius:3px; 1268} 1269 1270/* Link di dropdown mewarisi style .mega-cats a (warna, padding, radius). 1271 Kita tambahkan state active agar konsisten. */ 1272.mega-more-grid a.active { 1273 background:var(--brand-600); 1274 color:#fff; 1275 font-weight:600; 1276} 1277.hero { 1278 background:linear-gradient(135deg,var(--brand-50), #fff); 1279} 1280.hero-inner { 1281 display:grid; 1282 grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); 1283 gap:48px; 1284 align-items:center; 1285} 1286.hero-text h1 { 1287 font-size:48px; 1288 line-height:1.1; 1289 margin:0; 1290 font-weight:600; 1291 font-family:"Poppins",sans-serif; 1292} 1293.hero-text .text-brand { color:var(--brand-500); } 1294.lead { font-size:18px; color:var(--gray-700); margin-top:16px; } 1295.hero-cta { margin-top:28px; display:flex; gap:16px; } 1296.hero-bullets { margin-top:28px; display:flex; gap:28px; font-size:14px; color:var(--gray-600); } 1297.hero-box { 1298 background:#fff; 1299 padding:10px; 1300 border-radius:16px; 1301 box-shadow:0 4px 18px rgba(0,0,0,.08); 1302 /* min-height:320px; */ 1303 display:flex; 1304 align-items:center; 1305 justify-content:center; 1306} 1307.hero-box img {border-radius: 10px; } 1308 1309@media (max-width:680px) { 1310 .hero-inner { gap: 25px; } 1311} 1312 1313.usp-section { 1314 position:relative; 1315 background: 1316 linear-gradient(180deg,#F8FAFC 0%, #FFFFFF 60%) ; 1317 border-top:1px solid var(--border); 1318 border-bottom:1px solid var(--border); 1319 padding:10px 0 10px; 1320} 1321 1322.usp-list { 1323 list-style:none; 1324 margin:0; 1325 padding:0; 1326 display:grid; 1327 gap:26px; 1328} 1329 1330.usp-list.variant-soft { 1331 grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); 1332} 1333 1334.usp-item { 1335 display:flex; 1336 gap:16px; 1337 align-items:flex-start; 1338 background:#fff; 1339 padding:18px 18px 18px 16px; 1340 border:1px solid rgba(226,226,226,0.7); 1341 border-radius:18px; 1342 position:relative; 1343 overflow:hidden; 1344 isolation:isolate; 1345 box-shadow:0 2px 4px -1px rgba(0,0,0,.04), 0 6px 18px -6px rgba(0,0,0,.08); 1346 transition: .35s cubic-bezier(.16,.8,.32,1); 1347} 1348 1349.usp-item::before { 1350 content:""; 1351 position:absolute; 1352 inset:0; 1353 background:radial-gradient(circle at 18% 22%, rgba(201,162,39,.18), transparent 60%); 1354 opacity:.65; 1355 transition:.5s; 1356 z-index:-1; 1357} 1358 1359.usp-item:hover { 1360 transform:translateY(-6px); 1361 box-shadow:0 10px 26px -8px rgba(0,0,0,.15), 0 3px 8px -2px rgba(0,0,0,.08); 1362 border-color:rgba(201,162,39,.35); 1363} 1364 1365.usp-item:hover::before { 1366 background:radial-gradient(circle at 70% 40%, rgba(201,162,39,.22), transparent 65%); 1367} 1368 1369.usp-icon { 1370 flex:0 0 56px; 1371 width:56px; 1372 height:56px; 1373 border-radius:16px; 1374 display:flex; 1375 align-items:center; 1376 justify-content:center; 1377 background: 1378 linear-gradient(135deg,#FEF3C7,#FDE68A); 1379 position:relative; 1380 box-shadow:inset 0 0 0 1px rgba(255,255,255,.6), 0 4px 10px -3px rgba(0,0,0,.15); 1381 transition:.45s; 1382} 1383 1384.usp-item:hover .usp-icon { 1385 transform:rotate(-4deg) scale(1.06); 1386 box-shadow:inset 0 0 0 1px rgba(255,255,255,.8), 0 6px 14px -4px rgba(0,0,0,.20); 1387} 1388 1389.usp-icon svg { 1390 width:28px; 1391 height:28px; 1392 display:block; 1393 filter:drop-shadow(0 2px 2px rgba(0,0,0,.18));
1394} 1395 1396.usp-text h3 { 1397 font-size:15px; 1398 font-weight:600; 1399 margin:2px 0 6px; 1400 letter-spacing:.2px; 1401} 1402 1403.usp-text p { 1404 font-size:13px; 1405 line-height:1.3; 1406 margin:0; 1407 color:var(--gray-600); 1408} 1409 1410@media (max-width:680px) { 1411 .usp-section { padding: 5px 0 5px; margin-bottom: 0.75rem;} 1412 .usp-item { padding:16px 14px; } 1413 .usp-icon { width:50px;height:50px;border-radius:14px; } 1414 .usp-text h3 { font-size:14px; margin-bottom:4px; } 1415 .usp-text p { font-size:12.5px; } 1416 .usp-list { gap: 10px; } 1417} 1418 1419.flash { padding:50px 0 30px; background:#fff; } 1420.flash-head { 1421 display:flex; align-items:center; 1422 justify-content:space-between; 1423 margin-bottom:20px; 1424} 1425.flash-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1426.flash .countdown { 1427 font-family:monospace; 1428 background:#111; 1429 color:#fff; 1430 padding:8px 14px; 1431 border-radius:8px; 1432 font-size:16px; 1433 letter-spacing:1px; 1434} 1435.flash-scroll { 1436 display:flex; 1437 gap:16px; 1438 overflow-x:auto; 1439 padding-bottom:6px; 1440 scrollbar-width:none; 1441} 1442.flash-scroll::-webkit-scrollbar { display:none; } 1443 1444.products { padding:60px 0; background:#fff; } 1445.products-head { 1446 display:flex; justify-content:space-between; align-items:center; 1447 margin-bottom:24px; 1448 flex-wrap:wrap; 1449 gap:16px; 1450} 1451.products-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1452.product-filters input { 1453 padding:8px 12px; 1454 border:1px solid var(--border); 1455 border-radius:8px; 1456 font-size:14px; 1457 width:220px; 1458} 1459 1460.product-grid { 1461 display:grid; 1462 gap:24px; 1463 grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); 1464} 1465 1466.prod-card { 1467 background:#fff; 1468 border:1px solid var(--border); 1469 padding:12px; 1470 border-radius:12px; 1471 position:relative; 1472 display:flex; 1473 flex-direction:column; 1474 transition:var(--transition); 1475} 1476.prod-card.small { min-width:160px; } 1477.prod-card:hover { box-shadow:0 8px 20px rgba(0,0,0,.12); transform:translateY(-3px); } 1478.prod-card .img-box { 1479 background:var(--gray-100); 1480 border-radius:10px; 1481 aspect-ratio:1/1; 1482 display:flex; 1483 align-items:center; 1484 justify-content:center; 1485 overflow:hidden; 1486} 1487.prod-card .img-box img { width:100%; height:100%; object-fit:contain; transition:var(--transition); } 1488.prod-card:hover .img-box img { transform:scale(1.04); } 1489.prod-card .name { 1490 font-size:13.5px; 1491 font-weight:500; 1492 margin:10px 0 6px; 1493 min-height:36px; 1494 line-height:1.3; 1495} 1496.price { 1497 display:flex; 1498 gap:6px; 1499 align-items:center; 1500 flex-wrap:wrap; 1501} 1502.price strong { color:var(--brand-600); font-size:14px; } 1503.price .old { 1504 font-size:11px; 1505 color:var(--gray-500); 1506 text-decoration:line-through; 1507} 1508.tag.discount { 1509 position:absolute; 1510 top:8px; left:8px; 1511 background:#E54848; 1512 color:#fff; 1513 font-size:11px; 1514 padding:4px 6px; 1515 border-radius:6px; 1516 font-weight:600; 1517} 1518.btn { 1519 border:none; 1520 cursor:pointer; 1521 font-weight:500; 1522 font-size:14px; 1523 padding:12px 22px; 1524 border-radius:10px; 1525 background:var(--gray-900); 1526 color:#fff; 1527 transition:var(--transition); 1528 text-decoration:none; 1529 text-align:center; 1530 display:inline-flex; 1531 align-items:center; 1532 justify-content:center; 1533 gap:6px; 1534} 1535.btn:hover { background:#000; } 1536.btn-primary { background:var(--brand-500); } 1537.btn-primary:hover { background:var(--brand-600); } 1538.btn-outline { 1539 background:#fff; 1540 color:var(--gray-900); 1541 border:1px solid var(--border); 1542} 1543.btn-outline:hover { background:var(--gray-100); } 1544.btn-sm { padding:8px 14px; font-size:13px; } 1545.btn-mini { padding:6px 10px; font-size:12px; } 1546.add-cart { margin-top:auto; background:var(--brand-500); color:#fff; } 1547.add-cart:hover { background:var(--brand-600); } 1548 1549/* ==== FOOTER COMPACT FIX PATCH (final) ==== */ 1550 1551/* 1. Pastikan grid aktif */ 1552 1553.site-footer.footer-compact .ft-grid { 1554 display:grid; 1555 grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); 1556 gap:30px 40px; 1557 align-items:start; 1558} 1559 1560/* 2. Spacing atas bawah dipadatkan */ 1561.site-footer.footer-compact .ft-top { 1562 padding:42px 0 26px; 1563} 1564 1565/* 3. Judul & teks */ 1566.site-footer.footer-compact .ft-title { 1567 margin:0 0 14px; 1568 font-size:14px; 1569 letter-spacing:.8px; 1570 text-transform:uppercase; 1571 font-weight:600;
1572 color:#82b1c9; 1573} 1574 1575.site-footer.footer-compact .ft-desc, 1576.site-footer.footer-compact .ft-text { 1577 margin:0 0 16px; 1578 font-size:13.5px; 1579 line-height:1.45; 1580 color:#c6d6e0; 1581 max-width:330px; 1582} 1583.site-footer.footer-compact .ft-desc.sm-gap { margin-top:18px; } 1584 1585/* 4. Reset ikon SVG agar tidak membesar */ 1586.site-footer.footer-compact svg { 1587 width:20px; 1588 height:20px; 1589 flex:0 0 20px; 1590 display:inline-block; 1591} 1592.site-footer.footer-compact .ft-social svg { width:18px; height:18px; } 1593.site-footer.footer-compact .ft-wa-btn svg { width:18px; height:18px; } 1594 1595/* Jika ada aturan global svg { width:100%; } â paksa override */ 1596svg { max-width:100%; } 1597.site-footer.footer-compact svg { 1598 width:20px !important; 1599 height:20px !important; 1600} 1601 1602/* 5. Link list */ 1603.site-footer.footer-compact .ft-links { 1604 list-style:none; 1605 margin:0 0 12px; 1606 padding:0; 1607 display:flex; 1608 flex-direction:column; 1609 gap:4px; 1610} 1611.site-footer.footer-compact .ft-links li { margin:0; } 1612.site-footer.footer-compact .ft-links a { 1613 display:flex; 1614 align-items:center; 1615 gap:8px; 1616 font-size:13.5px; 1617 padding:5px 2px; 1618 text-decoration:none; 1619 color:#c6d6e0; 1620 border-radius:8px; 1621 transition:.25s; 1622} 1623.site-footer.footer-compact .ft-links a:hover { 1624 background:rgba(255,255,255,.06); 1625 padding-left:6px; 1626 color:#fff; 1627} 1628 1629.site-footer.footer-compact .ico-s { 1630 width:24px; 1631 height:24px; 1632 background:#103346; 1633 color:#89b8cf; 1634 border-radius:6px; 1635 display:flex; 1636 align-items:center; 1637 justify-content:center; 1638 flex:0 0 24px; 1639 font-size:12px; 1640 transition:.3s; 1641} 1642.site-footer.footer-compact .ft-links a:hover .ico-s { 1643 background:#C9A227; 1644 color:#fff; 1645 transform:rotate(-4deg); 1646} 1647 1648/* 6. WhatsApp button */ 1649.site-footer.footer-compact .ft-wa-btn { 1650 display:inline-flex; 1651 align-items:center; 1652 gap:8px; 1653 background:#fff; 1654 color:#062437; 1655 padding:8px 18px; 1656 font-size:13.5px; 1657 border-radius:22px;
1658 font-weight:500; 1659 text-decoration:none; 1660 box-shadow:0 3px 8px -3px rgba(0,0,0,.45); 1661 transition:.25s; 1662} 1663.site-footer.footer-compact .ft-wa-btn:hover { 1664 transform:translateY(-2px); 1665 box-shadow:0 6px 14px -6px rgba(0,0,0,.55); 1666} 1667 1668/* 7. Sosial */ 1669.site-footer.footer-compact .ft-social { 1670 display:flex; 1671 gap:12px; 1672 margin-top:4px; 1673} 1674.site-footer.footer-compact .ft-social a { 1675 width:34px; 1676 height:34px; 1677 background:rgba(255,255,255,.08); 1678 display:flex; 1679 align-items:center; 1680 justify-content:center; 1681 border-radius:10px; 1682 color:#c6d6e0; 1683 transition:.25s; 1684} 1685.site-footer.footer-compact .ft-social a:hover { 1686 background:#C9A227; 1687 color:#fff; 1688} 1689 1690/* 8. Alamat */ 1691.site-footer.footer-compact .ft-address { 1692 font-size:12.5px; 1693 line-height:1.44; 1694 color:#8fa4b1; 1695 max-width:330px; 1696 margin-top:12px; 1697} 1698.site-footer.footer-compact .ft-address .company { 1699 margin:0 0 4px; 1700 font-weight:600; 1701 color:#e5f2f7; 1702 font-size:12.8px; 1703} 1704 1705/* 9. Payments kecil & rapi */ 1706.site-footer.footer-compact .ft-payments { 1707 margin-top:14px; 1708 display:flex; 1709 flex-direction:column; 1710 gap:8px; 1711 max-width:350px; 1712} 1713.site-footer.footer-compact .ft-payments .pay-row { 1714 display:flex; 1715 gap:8px; 1716 flex-wrap:wrap; 1717} 1718.site-footer.footer-compact .ft-payments img { 1719 width:46px; 1720 height:32px; 1721 padding:3px 5px; 1722 background:#fff; 1723 border-radius:7px; 1724 object-fit:contain; 1725 box-shadow:0 2px 6px -3px rgba(0,0,0,.45); 1726 transition:.25s; 1727} 1728.site-footer.footer-compact .ft-payments img:hover { 1729 transform:translateY(-3px); 1730 box-shadow:0 5px 12px -6px rgba(0,0,0,.55); 1731} 1732 1733/* 10. Bottom bar */ 1734.site-footer.footer-compact .ft-bottom { 1735 border-top:1px solid rgba(255,255,255,.08); 1736 background:#041f31; 1737 padding:12px 0 16px; 1738 margin-top:4px; 1739} 1740.site-footer.footer-compact .ft-copy { 1741 font-size:11.5px; 1742 color:#8fa7b5; 1743 display:flex; 1744 flex-wrap:wrap; 1745 gap:12px; 1746 justify-content:center; 1747 line-height:1.3; 1748 margin:0; 1749} 1750.site-footer.footer-compact .ft-copy span { 1751 position:relative; 1752 padding:0 10px; 1753} 1754.site-footer.footer-compact .ft-copy span+span:before { 1755 content:""; 1756 position:absolute; 1757 left:0; 1758 top:50%; 1759 width:1px; 1760 height:12px; 1761 background:rgba(255,255,255,.14); 1762 transform:translateY(-50%); 1763} 1764@media (max-width:580px){ 1765 .site-footer.footer-compact .ft-top { padding:34px 0 18px; } 1766 .site-footer.footer-compact .ft-grid { gap:26px 20px; } 1767 .site-footer.footer-compact .ft-copy span { padding:0; } 1768 .site-footer.footer-compact .ft-copy span+span:before { display:none; } 1769} 1770.copy { 1771 margin-top:40px; 1772 border-top:1px solid #222; 1773 text-align:center; 1774 padding:18px 0; 1775 font-size:12px; 1776 color:#666; 1777} 1778 1779@media (max-width:760px) { 1780 .hero-text h1 { font-size:38px; } 1781 .hero { padding:0px 0 30px; } 1782 .topbar-inner { flex-direction:column; gap:4px; padding:6px 0; } 1783 .nav-inner { flex-wrap:wrap; } 1784 .mega-cats { gap:10px; } 1785 .product-grid { gap:16px; } 1786 .prod-card { padding:10px; } 1787} 1788 1789/* === PLAZA IT DARK ORANGE FOOTER THEME === */ 1790.site-footer.footer-compact { 1791 --ft-bg-0:#051722; 1792 --ft-bg-1:#072534; 1793 --ft-bg-2:#0B2F40; 1794 --ft-bg-3:#103A4E; 1795 --ft-gold:#C9A227; 1796 --ft-gold-dark:#8B6914; 1797 --ft-text:#E9F2F6; 1798 --ft-text-sec:#B8CAD3; 1799 --ft-text-muted:#8FA4B1; 1800 --ft-divider:rgba(255,255,255,0.08); 1801 --ft-glow:rgba(201,162,39,0.18); 1802 --ft-glow-fade:rgba(201,162,39,0); 1803 color:var(--ft-text); 1804} 1805 1806/* BASE TEXT / LINKS (override sebelumnya jika perlu) */ 1807.site-footer.footer-compact .ft-title { color:#9CC5D7; } 1808.site-footer.footer-compact .ft-links a { color:var(--ft-text-sec); } 1809.site-footer.footer-compact .ft-links a:hover { color:#fff; } 1810.site-footer.footer-compact .ft-address { color:var(--ft-text-muted); } 1811.site-footer.footer-compact .ft-copy { color:#9AB2BD; } 1812 1813/* ICON BACKGROUND (kubu gelap) */ 1814.site-footer.footer-compact .ico-s { 1815 background:#0F3647; 1816 color:#9FC7DA; 1817} 1818.site-footer.footer-compact .ft-links a:hover .ico-s { 1819 background:var(--ft-gold); 1820 color:#fff; 1821} 1822 1823/* WhatsApp button biarkan putih agar kontras */ 1824.site-footer.footer-compact .ft-wa-btn { 1825 box-shadow:0 4px 12px -5px rgba(0,0,0,.6), 0 0 0 3px rgba(255,255,255,0.05); 1826} 1827.site-footer.footer-compact .ft-wa-btn:hover { 1828 box-shadow:0 8px 20px -8px rgba(0,0,0,.65), 0 0 0 3px rgba(201,162,39,0.25); 1829} 1830 1831/* Payment cards border halus */ 1832.site-footer.footer-compact .ft-payments img { 1833 box-shadow:0 2px 6px -3px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,0.05); 1834} 1835.site-footer.footer-compact .ft-payments img:hover { 1836 box-shadow:0 6px 14px -6px rgba(0,0,0,.65), 0 0 0 1px rgba(201,162,39,0.4); 1837} 1838 1839/* ========== VARIANT A: DIAGONAL EMBER (AKTIF) ========== */ 1840.site-footer.footer-compact.variant-ember { 1841 background: 1842 radial-gradient(circle at 78% 82%, var(--ft-glow) 0%, var(--ft-glow-fade) 55%), 1843 linear-gradient(140deg,var(--ft-bg-1) 0%, var(--ft-bg-2) 55%, var(--ft-bg-0) 100%); 1844} 1845.site-footer.footer-compact.variant-ember .ft-bottom { 1846 background:rgba(0,0,0,0.25); 1847 backdrop-filter:blur(2px); 1848} 1849 1850/* ========== VARIANT B: TOP BAR BLUE ========== */ 1851.site-footer.footer-compact.variant-bar { 1852 background:var(--ft-bg-1); 1853 position:relative; 1854} 1855.site-footer.footer-compact.variant-bar::before { 1856 content:""; 1857 position:absolute; 1858 left:0;top:0; 1859 width:100%;height:3px; 1860 background:linear-gradient(90deg,var(--ft-gold-dark),var(--ft-gold)); 1861 box-shadow:0 0 12px -2px var(--ft-gold); 1862} 1863.site-footer.footer-compact.variant-bar .ft-bottom { 1864 background:#041c28; 1865} 1866 1867/* ========== VARIANT C: RADIAL GLOW BAWAH ========== */ 1868.site-footer.footer-compact.variant-radial { 1869 background: 1870 radial-gradient(circle at 70% 90%, rgba(201,162,39,0.22) 0%, rgba(201,162,39,0.04) 38%, rgba(201,162,39,0) 65%), 1871 linear-gradient(150deg,#052131 0%, #062d3f 50%, #041924 100%); 1872} 1873.site-footer.footer-compact.variant-radial .ft-bottom { 1874 background:#041c29; 1875} 1876
1877/* ========== VARIANT D: LAYERED STRIPES ========== */ 1878.site-footer.footer-compact.variant-stripes { 1879 background: 1880 repeating-linear-gradient(180deg, rgba(255,255,255,0.015) 0 38px, rgba(255,255,255,0) 38px 76px), 1881 linear-gradient(130deg,#052636 0%, #062a3c 60%, #041d28 100%); 1882 position:relative; 1883} 1884.site-footer.footer-compact.variant-stripes::after { 1885 content:""; 1886 position:absolute; 1887 left:0;bottom:0; 1888 width:100%;height:110px; 1889 background:linear-gradient(0deg,rgba(0,0,0,.35),rgba(0,0,0,0)); 1890 pointer-events:none; 1891} 1892.site-footer.footer-compact.variant-stripes .ft-bottom { 1893 background:#031821; 1894} 1895 1896/* Garis aksen biru tipis di atas bottom */ 1897.site-footer.footer-compact.variant-stripes .ft-bottom { 1898 position:relative; 1899} 1900.site-footer.footer-compact.variant-stripes .ft-bottom::before { 1901 content:""; 1902 position:absolute; 1903 left:0;top:0; 1904 width:100%;height:2px; 1905 background:linear-gradient(90deg,rgba(201,162,39,0),rgba(201,162,39,.65),rgba(201,162,39,0)); 1906} 1907 1908/* ========== VARIANT E: GLASS PANEL ========== */ 1909.site-footer.footer-compact.variant-glass { 1910 background:linear-gradient(145deg,#292205 0%, #292205 65%, #292205 100%); 1911} 1912.site-footer.footer-compact.variant-glass .ft-grid { 1913 position:relative; 1914 padding:32px clamp(10px,2vw,24px); 1915 border-radius:22px;
1916 background:linear-gradient(140deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02)); 1917 border:1px solid rgba(255,255,255,0.08); 1918 box-shadow:0 12px 28px -12px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04) inset; 1919 backdrop-filter:blur(8px); 1920 -webkit-backdrop-filter:blur(8px); 1921} 1922.site-footer.footer-compact.variant-glass .ft-bottom { 1923 background:#02141d; 1924} 1925 1926/* HOVER ACCENTS */ 1927.site-footer.footer-compact a { 1928 transition:color .25s, background .25s, box-shadow .25s; 1929} 1930.site-footer.footer-compact .ft-links a:hover { 1931 box-shadow:0 2px 6px -3px rgba(0,0,0,.5), 1932 0 0 0 1px rgba(201,162,39,0.55); 1933} 1934.site-footer.footer-compact .ft-social a { 1935 background:#0E3445; 1936 color:#bcd4dd; 1937} 1938.site-footer.footer-compact .ft-social a:hover { 1939 background:var(--ft-gold); 1940 color:#fff; 1941 box-shadow:0 0 0 3px rgba(201,162,39,0.28); 1942} 1943 1944/* Divider di atas footer (opsional) */ 1945.footer-divider-top { 1946 height:1px; 1947 background:linear-gradient(90deg,transparent,rgba(255,255,255,0.15),transparent); 1948 width:100%; 1949 margin:0 0 4px; 1950} 1951 1952/* ==== MOBILE FOOTER COLLAPSE (Non-Homepage Only) ==== */ 1953 1954/* Desktop: wrapper tidak mengganggu grid layout */ 1955.ft-collapsible-content { 1956 display: contents; 1957} 1958 1959@media (max-width: 768px) { 1960 /* Wrapper untuk konten yang bisa collapse */ 1961 .ft-top.ft-collapsible .ft-collapsible-content { 1962 display: block; 1963 max-height: 80px; 1964 overflow: hidden; 1965 position: relative; 1966 transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1967 } 1968 1969 /* Fade gradient overlay */ 1970 .ft-top.ft-collapsible .ft-collapsible-content::after { 1971 content: ''; 1972 position: absolute; 1973 bottom: 0; 1974 left: 0; 1975 right: 0; 1976 height: 70px; 1977 background: linear-gradient(to bottom, transparent 0%, rgba(6, 43, 69, 0.6) 40%, #062b45 100%); 1978 pointer-events: none; 1979 opacity: 1; 1980 transition: opacity 0.3s ease; 1981 z-index: 1; 1982 } 1983 1984 /* State: Expanded */ 1985 .ft-top.ft-collapsible.expanded .ft-collapsible-content { 1986 max-height: 1500px; 1987 } 1988 1989 .ft-top.ft-collapsible.expanded .ft-collapsible-content::after { 1990 opacity: 0; 1991 } 1992 1993 /* Expand Button - Di luar collapsible, selalu visible */ 1994 .ft-expand-btn { 1995 display: flex;
1996 align-items: center; 1997 justify-content: center; 1998 gap: 8px; 1999 width: 100%; 2000 margin: -8px 0 0; 2001 padding: 12px 20px; 2002 background: rgba(255,255,255,0.12); 2003 border: 1px solid rgba(255,255,255,0.25); 2004 border-radius: 10px; 2005 color: #e5f2f7; 2006 font-size: 13.5px; 2007 font-weight: 500; 2008 cursor: pointer; 2009 transition: all 0.3s ease; 2010 outline: none; 2011 position: relative; 2012 z-index: 10; 2013 } 2014 2015 .ft-expand-btn:hover { 2016 background: rgba(255,255,255,0.18); 2017 border-color: rgba(255,255,255,0.35); 2018 color: #fff; 2019 transform: translateY(-2px); 2020 } 2021 2022 .ft-expand-btn:active { 2023 transform: scale(0.98); 2024 } 2025 2026 .ft-expand-btn svg { 2027 width: 18px; 2028 height: 18px; 2029 transition: transform 0.3s ease; 2030 fill: currentColor; 2031 } 2032 2033 .ft-top.ft-collapsible.expanded .ft-expand-btn { 2034 margin-top: 15px; 2035 } 2036 2037 .ft-top.ft-collapsible.expanded .ft-expand-btn svg { 2038 transform: rotate(180deg); 2039 } 2040} 2041 2042/* Desktop: hide expand button */ 2043@media (min-width: 769px) { 2044 .ft-expand-btn { 2045 display: none !important; 2046 } 2047} 2048 2049/* RESPONSIVE (jika background panel butuh penyesuaian) */ 2050@media (max-width:640px){ 2051 .site-footer.footer-compact.variant-glass .ft-grid { 2052 padding:26px 18px; 2053 } 2054} 2055 2056</style> 2057 <style data-inlined="bottom-nav.css"> 2058/* Mobile Bottom Navigation */ 2059.bottom-nav { 2060 position: fixed; 2061 bottom: 0; 2062 left: 0; 2063 right: 0; 2064 background: #ffffff; 2065 border-top: 1px solid #e5e7eb; 2066 z-index: 9998; /* Below chat widget */ 2067 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); 2068 display: none; /* Hidden on desktop */ 2069} 2070 2071.bottom-nav-inner { 2072 display: flex; 2073 justify-content: space-around; 2074 align-items: center; 2075 padding: 2px 0; 2076 max-width: 640px; 2077 margin: 0 auto; 2078} 2079 2080.bottom-nav-item { 2081 flex: 1; 2082 display: flex; 2083 flex-direction: column; 2084 align-items: center; 2085 justify-content: center; 2086 gap: 4px; 2087 padding: 8px 8px; 2088 background: none; 2089 border: none; 2090 cursor: pointer; 2091 text-decoration: none; 2092 color: #6b7280; 2093 transition: all 0.3s ease; 2094 position: relative; 2095} 2096 2097.bottom-nav-item:hover { 2098 color: #C9A227; 2099} 2100 2101.bottom-nav-item.active { 2102 flex-direction: row; 2103 gap: 8px; 2104 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 2105 color: white; 2106 border-radius: 20px; 2107 padding: 8px 16px; 2108 box-shadow: 0 2px 8px rgba(201, 162, 39, 0.3); 2109} 2110 2111.bottom-nav-item-icon { 2112 width: 24px; 2113 height: 24px; 2114 position: relative; 2115} 2116 2117.bottom-nav-item-icon svg { 2118 width: 100%; 2119 height: 100%; 2120} 2121 2122/* Icon variant switching */ 2123.bottom-nav-item .icon-outlined { 2124 display: block; 2125} 2126 2127.bottom-nav-item .icon-filled { 2128 display: none; 2129} 2130 2131.bottom-nav-item.active .icon-outlined { 2132 display: none; 2133} 2134 2135.bottom-nav-item.active .icon-filled { 2136 display: block; 2137} 2138 2139.bottom-nav-item-label { 2140 font-size: 12px; 2141 font-weight: 600; 2142 line-height: 1; 2143 display: none; 2144 white-space: nowrap; 2145} 2146 2147.bottom-nav-item.active .bottom-nav-item-label { 2148 display: block; 2149} 2150 2151/* Avatar styles for bottom-nav */ 2152.bottom-nav-avatar-img, 2153.bottom-nav-avatar-initials { 2154 width: 24px; 2155 height: 24px; 2156 border-radius: 50%; 2157 display: block; 2158} 2159 2160.bottom-nav-avatar-img { 2161 object-fit: cover; 2162} 2163 2164.bottom-nav-avatar-initials { 2165 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 2166 color: white; 2167 font-size: 10px; 2168 font-weight: 700; 2169 display: flex; 2170 align-items: center; 2171 justify-content: center; 2172 line-height: 1; 2173 letter-spacing: 0.5px; 2174} 2175 2176/* Hide SVG when avatar is shown */ 2177.bottom-nav-item-icon:has(.bottom-nav-avatar-img) .bottom-nav-svg-icon, 2178.bottom-nav-item-icon:has(.bottom-nav-avatar-initials) .bottom-nav-svg-icon { 2179 display: none; 2180} 2181 2182/* Active state adjustments for avatar */ 2183.bottom-nav-item.active .bottom-nav-avatar-initials { 2184 background: white; 2185 color: #C9A227; 2186 border: 2px solid rgba(255, 255, 255, 0.8); 2187 box-sizing: border-box; 2188} 2189 2190.bottom-nav-item.active .bottom-nav-avatar-img { 2191 border: 2px solid rgba(255, 255, 255, 0.9); 2192 box-sizing: border-box; 2193} 2194 2195.bottom-nav-item-badge { 2196 position: absolute; 2197 top: -4px; 2198 right: -4px; 2199 background: #ef4444; 2200 color: white; 2201 border-radius: 10px; 2202 padding: 2px 5px; 2203 font-size: 10px; 2204 font-weight: 600; 2205 min-width: 18px;
2206 height: 18px; 2207 display: flex; 2208 align-items: center; 2209 justify-content: center; 2210 line-height: 1; 2211} 2212 2213/* Show on mobile only */ 2214@media (max-width: 640px) { 2215 .bottom-nav { 2216 display: block; 2217 } 2218 2219 /* Add bottom padding to body to prevent content being hidden */ 2220 body { 2221 padding-bottom: 60px; 2222 } 2223} 2224 2225/* Mobile User Sheet - Bottom Sheet for Profile Menu */ 2226.mobile-user-sheet { 2227 position: fixed; 2228 bottom: 0; 2229 left: 0; 2230 right: 0; 2231 z-index: 10000; 2232 pointer-events: none; 2233} 2234 2235.mobile-user-sheet:not([hidden]) { 2236 pointer-events: auto; 2237} 2238 2239.mobile-user-sheet-backdrop { 2240 position: fixed; 2241 inset: 0; 2242 background: rgba(0, 0, 0, 0.5); 2243 opacity: 0; 2244 transition: opacity 0.2s ease-out; 2245} 2246 2247.mobile-user-sheet.active .mobile-user-sheet-backdrop { 2248 opacity: 1; 2249} 2250 2251.mobile-user-sheet-content { 2252 position: absolute; 2253 bottom: 0; 2254 left: 0; 2255 right: 0; 2256 background: white; 2257 border-radius: 16px 16px 0 0; 2258 max-height: 80vh; 2259 overflow-y: auto; 2260 transform: translateY(100%); 2261 transition: transform 0.3s ease-out; 2262} 2263 2264.mobile-user-sheet.active .mobile-user-sheet-content { 2265 transform: translateY(0); 2266} 2267 2268.mobile-user-sheet-handle { 2269 width: 40px; 2270 height: 4px; 2271 background: #d1d5db; 2272 border-radius: 2px; 2273 margin: 12px auto 16px; 2274} 2275 2276.mobile-user-sheet-header { 2277 padding: 0 20px 16px; 2278 border-bottom: 1px solid #e5e7eb; 2279} 2280 2281.mobile-user-info { 2282 display: flex; 2283 align-items: center; 2284 gap: 12px; 2285} 2286 2287.mobile-user-avatar { 2288 width: 48px; 2289 height: 48px; 2290 background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 50%, #FCD34D 100%); 2291 border-radius: 50%; 2292 display: flex; 2293 align-items: center; 2294 justify-content: center; 2295 font-size: 24px; 2296 color: #8B6914; 2297 font-weight: 600; 2298} 2299 2300.mobile-user-details { 2301 flex: 1; 2302} 2303 2304.mobile-user-name { 2305 font-weight: 600; 2306 font-size: 16px; 2307 color: #1f2937; 2308 margin-bottom: 4px; 2309} 2310 2311.mobile-user-email { 2312 font-size: 13px; 2313 color: #6b7280; 2314} 2315 2316.mobile-user-sheet-body { 2317 padding: 8px 0; 2318} 2319 2320.mobile-user-sheet-item { 2321 display: flex; 2322 align-items: center; 2323 gap: 12px; 2324 padding: 14px 20px; 2325 background: none; 2326 border: none; 2327 width: 100%; 2328 text-align: left; 2329 cursor: pointer; 2330 text-decoration: none; 2331 color: #374151; 2332 transition: background 0.2s; 2333} 2334 2335.mobile-user-sheet-item:hover { 2336 background: #f9fafb; 2337} 2338 2339.mobile-user-sheet-icon { 2340 font-size: 20px; 2341 width: 24px; 2342 text-align: center; 2343} 2344 2345.mobile-user-sheet-label { 2346 font-size: 15px; 2347 font-weight: 500; 2348} 2349 2350.mobile-user-sheet-divider { 2351 height: 1px; 2352 background: #e5e7eb; 2353 margin: 8px 0; 2354} 2355 2356@keyframes fadeIn { 2357 from { 2358 opacity: 0; 2359 } 2360 to { 2361 opacity: 1; 2362 } 2363} 2364 2365@keyframes slideUp { 2366 from { 2367 transform: translateY(100%); 2368 } 2369 to { 2370 transform: translateY(0); 2371 } 2372} 2373 2374/* Ensure chat widget works well with bottom nav */ 2375@media (max-width: 640px) { 2376 .chat-widget { 2377 bottom: 0 !important; /* Chat opens from bottom */ 2378 } 2379 2380 .chat-popup { 2381 /* Chat popup should be above bottom nav */ 2382 z-index: 9999; 2383 } 2384} 2385 2386</style> 2387 <style data-inlined="search-suggest.css"> 2388.search { 2389 position: relative; 2390} 2391 2392/* AI Bot Toggle Button */ 2393.ai-bot-toggle { 2394 display: flex; 2395 align-items: center; 2396 justify-content: center; 2397 background: transparent; 2398 border: none; 2399 padding: 8px; 2400 cursor: pointer; 2401 border-radius: 8px; 2402 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 2403 margin-left: 4px; 2404 flex-shrink: 0; 2405 display: none; 2406} 2407 2408.ai-bot-toggle .bot-icon { 2409 color: #9ca3af; 2410 transition: all 0.3s ease; 2411} 2412 2413.ai-bot-toggle:hover .bot-icon { 2414 color: #6b7280; 2415 transform: scale(1.1); 2416} 2417 2418.ai-bot-toggle.active .bot-icon { 2419 color: #FBBF24; 2420 animation: botPulse 2s ease-in-out infinite; 2421} 2422 2423.ai-bot-toggle.active { 2424 background: rgba(251, 191, 36, 0.1); 2425} 2426 2427.ai-bot-toggle.active:hover .bot-icon { 2428 color: #CA8A04; 2429} 2430 2431@keyframes botPulse { 2432 0%, 100% { transform: scale(1); } 2433 50% { transform: scale(1.12); } 2434} 2435 2436/* Search Suggest Box */ 2437.search-suggest { 2438 position: absolute; 2439 top: 100%; left: 0; right: 0; 2440 background: #fff; 2441 border: 1px solid #e1e4e8; 2442 border-top: none; 2443 box-shadow: 0 4px 12px rgba(0,0,0,0.08); 2444 max-height: 400px; 2445 overflow-y: auto; 2446 z-index: 100; 2447 padding: 0; 2448 font-size: 14px; 2449 border-radius: 0 0 8px 8px; 2450} 2451 2452.suggest-item { 2453 display: flex; 2454 align-items: center; 2455 gap: 8px; 2456 padding: 10px 16px; 2457 cursor: pointer; 2458 transition: all 0.2s ease; 2459 border-bottom: 1px solid #f1f5f9; 2460} 2461 2462.suggest-item:last-child { 2463 border-bottom: none; 2464} 2465 2466.suggest-item:hover, 2467.suggest-item.active { 2468 background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); 2469 padding-left: 20px; 2470} 2471 2472.suggest-thumb { 2473 width: 42px; 2474 height: 42px; 2475 object-fit: cover; 2476 border: 1px solid #ddd; 2477 border-radius: 4px; 2478 background: #fafafa; 2479} 2480 2481.suggest-meta { flex: 1; min-width: 0; } 2482 2483.suggest-name { 2484 font-weight: 500;
2485 color: #222; 2486 line-height: 1.2; 2487 white-space: nowrap; 2488 overflow: hidden; 2489 text-overflow: ellipsis; 2490} 2491 2492.suggest-price { 2493 color: #d32f2f; 2494 font-size: 12px; 2495 margin-top: 2px; 2496} 2497 2498/* AI Insights */ 2499.suggest-ai-insight { 2500 display: flex; 2501 align-items: center; 2502 gap: 8px; 2503 padding: 8px 12px; 2504 background: linear-gradient(135deg, #FFFBEB 0%, #FEF9C3 100%); 2505 border-bottom: 1px solid #FDE68A; 2506 color: #92400E; 2507 font-size: 12px; 2508 font-weight: 500; 2509 line-height: 1.4; 2510} 2511 2512.suggest-ai-insight svg { 2513 flex-shrink: 0; 2514 opacity: 0.8; 2515} 2516 2517.suggest-ai-insight span { 2518 flex: 1; 2519} 2520 2521/* AI Progress States */ 2522.ai-progress-container { 2523 padding: 20px 16px; 2524 background: linear-gradient(135deg, #fafbff 0%, #f5f7ff 100%); 2525} 2526 2527.ai-progress-header { 2528 display: flex; 2529 align-items: center; 2530 gap: 10px; 2531 margin-bottom: 16px; 2532} 2533 2534.ai-progress-icon { 2535 width: 36px; 2536 height: 36px; 2537 display: flex; 2538 align-items: center; 2539 justify-content: center; 2540 background: linear-gradient(135deg, #EAB308 0%, #8B6914 100%); 2541 border-radius: 50%; 2542 color: #fff; 2543 font-size: 18px; 2544 animation: aiIconSpin 3s linear infinite; 2545} 2546 2547@keyframes aiIconSpin { 2548 0% { transform: rotate(0deg); } 2549 100% { transform: rotate(360deg); } 2550} 2551 2552.ai-progress-text { 2553 flex: 1; 2554} 2555 2556.ai-progress-title { 2557 font-weight: 600; 2558 color: #1e293b; 2559 font-size: 14px; 2560 margin-bottom: 2px; 2561} 2562 2563.ai-progress-subtitle { 2564 font-size: 12px; 2565 color: #64748b; 2566} 2567 2568/* Progress Steps */ 2569.ai-progress-steps { 2570 display: flex; 2571 flex-direction: column; 2572 gap: 10px; 2573 margin-top: 16px; 2574} 2575 2576.ai-progress-step { 2577 display: flex; 2578 align-items: center; 2579 gap: 10px; 2580 padding: 8px 12px; 2581 background: #fff; 2582 border-radius: 8px; 2583 border: 1px solid #e2e8f0; 2584 transition: all 0.3s ease; 2585} 2586 2587.ai-progress-step.active { 2588 border-color: #EAB308; 2589 background: #f8f9ff; 2590 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); 2591} 2592 2593.ai-progress-step.completed { 2594 border-color: #10b981; 2595 background: #f0fdf4; 2596} 2597 2598.ai-step-icon { 2599 width: 20px; 2600 height: 20px; 2601 display: flex; 2602 align-items: center; 2603 justify-content: center; 2604 border-radius: 50%; 2605 font-size: 11px; 2606 flex-shrink: 0; 2607} 2608 2609.ai-progress-step.active .ai-step-icon { 2610 background: #EAB308; 2611 color: #fff; 2612 animation: pulse 1.5s ease-in-out infinite; 2613} 2614 2615.ai-progress-step.completed .ai-step-icon { 2616 background: #10b981; 2617 color: #fff; 2618} 2619 2620.ai-progress-step.pending .ai-step-icon { 2621 background: #e2e8f0; 2622 color: #94a3b8; 2623} 2624 2625.ai-step-text { 2626 flex: 1; 2627 font-size: 13px; 2628 color: #475569; 2629 font-weight: 500; 2630} 2631 2632.ai-progress-step.completed .ai-step-text { 2633 color: #059669; 2634} 2635 2636/* AI Analysis Result Banner */ 2637.ai-result-banner { 2638 padding: 12px 16px; 2639 background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); 2640 border-bottom: 1px solid #86efac; 2641 display: flex; 2642 align-items: center; 2643 gap: 10px; 2644} 2645 2646.ai-result-icon { 2647 font-size: 20px; 2648 animation: bounceIn 0.5s ease; 2649} 2650 2651@keyframes bounceIn { 2652 0% { transform: scale(0); opacity: 0; } 2653 50% { transform: scale(1.1); } 2654 100% { transform: scale(1); opacity: 1; } 2655} 2656 2657.ai-result-text { 2658 flex: 1; 2659} 2660 2661.ai-result-title { 2662 font-weight: 600; 2663 color: #065f46; 2664 font-size: 13px; 2665 margin-bottom: 2px; 2666} 2667 2668.ai-result-description { 2669 font-size: 12px; 2670 color: #047857; 2671 opacity: 0.9; 2672} 2673 2674/* Product Items Section Header */ 2675.suggest-section-header { 2676 padding: 10px 16px; 2677 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); 2678 border-bottom: 1px solid #e2e8f0; 2679} 2680 2681.suggest-header-main { 2682 display: flex; 2683 align-items: center; 2684 gap: 6px; 2685 margin-bottom: 4px; 2686} 2687 2688.suggest-header-icon { 2689 font-size: 14px; 2690} 2691 2692.suggest-header-text { 2693 font-size: 12px; 2694 font-weight: 600; 2695 color: #64748b; 2696 text-transform: uppercase; 2697 letter-spacing: 0.5px; 2698} 2699 2700.suggest-header-hint { 2701 font-size: 11px; 2702 color: #94a3b8; 2703 font-weight: 400; 2704 text-transform: none; 2705 letter-spacing: 0; 2706 display: flex; 2707 align-items: center; 2708 gap: 4px; 2709} 2710 2711.suggest-header-hint kbd { 2712 display: inline-block; 2713 padding: 2px 6px; 2714 font-size: 10px; 2715 font-weight: 600; 2716 line-height: 1; 2717 color: #475569; 2718 background: #fff; 2719 border: 1px solid #cbd5e1; 2720 border-radius: 4px; 2721 box-shadow: 0 1px 2px rgba(0,0,0,0.05); 2722 font-family: 'Segoe UI', monospace; 2723} 2724 2725/* Footer Hint */ 2726.suggest-footer-hint { 2727 display: flex; 2728 align-items: center; 2729 justify-content: center; 2730 padding: 12px 16px; 2731 background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%); 2732 border-top: 1px solid #EAB308; 2733 color: #8B6914; 2734 font-size: 12px; 2735 font-weight: 500; 2736 text-align: center; 2737 cursor: default; 2738} 2739 2740.suggest-footer-hint svg { 2741 color: #CA8A04; 2742 flex-shrink: 0; 2743} 2744 2745.suggest-footer-hint kbd { 2746 display: inline-block; 2747 padding: 3px 7px; 2748 margin: 0 3px; 2749 font-size: 11px; 2750 font-weight: 600; 2751 line-height: 1; 2752 color: #78350F; 2753 background: #fff; 2754 border: 1px solid #CA8A04; 2755 border-radius: 4px; 2756 box-shadow: 0 1px 3px rgba(245, 158, 11, 0.2); 2757 font-family: 'Segoe UI', monospace; 2758} 2759 2760.suggest-footer-link { 2761 color: #CA8A04; 2762 font-weight: 600; 2763 text-decoration: underline; 2764 text-underline-offset: 2px; 2765 text-decoration-thickness: 1px; 2766 cursor: pointer; 2767 transition: all 0.2s ease; 2768} 2769 2770.suggest-footer-link:hover { 2771 color: #8B6914; 2772 text-decoration-thickness: 2px; 2773 text-shadow: 0 0 8px rgba(234, 88, 12, 0.3); 2774} 2775 2776/* Loading state */ 2777.suggest-loading { 2778 display: flex; 2779 align-items: center; 2780 justify-content: center; 2781 gap: 8px; 2782 padding: 16px 12px; 2783 color: #64748b; 2784 font-size: 13px; 2785} 2786 2787@keyframes pulse { 2788 0%, 100% { opacity: 1; } 2789 50% { opacity: 0.5; } 2790} 2791 2792.suggest-loading { 2793 animation: pulse 1.5s ease-in-out infinite; 2794} 2795 2796/* Responsive Mobile Styles */ 2797@media (max-width: 768px) { 2798 .ai-bot-toggle { 2799 padding: 6px; 2800 margin-left: 0; 2801 } 2802 2803 .ai-bot-toggle .bot-icon {
2804 width: 20px; 2805 height: 20px; 2806 } 2807 2808 .search-icon-btn { 2809 margin-right: 4px; 2810 } 2811 2812 .search-icon-btn svg { 2813 width: 18px; 2814 height: 18px; 2815 } 2816 2817 .search-suggest { 2818 max-height: 350px; 2819 } 2820 2821 /* Mobile hint adjustments */ 2822 .suggest-header-hint { 2823 font-size: 10px; 2824 } 2825 2826 .suggest-header-hint kbd { 2827 padding: 1px 4px; 2828 font-size: 9px; 2829 } 2830 2831 .suggest-footer-hint { 2832 padding: 10px 12px; 2833 font-size: 11px; 2834 } 2835 2836 .suggest-footer-hint svg { 2837 width: 14px; 2838 height: 14px; 2839 } 2840 2841 .suggest-footer-hint kbd { 2842 padding: 2px 5px; 2843 font-size: 10px; 2844 } 2845 2846 .ai-progress-container { 2847 padding: 16px 12px; 2848 } 2849 2850 .ai-progress-icon { 2851 width: 32px; 2852 height: 32px; 2853 font-size: 16px; 2854 } 2855 2856 .ai-progress-title { 2857 font-size: 13px; 2858 } 2859 2860 .ai-progress-subtitle { 2861 font-size: 11px; 2862 } 2863 2864 .ai-progress-step { 2865 padding: 6px 10px; 2866 } 2867 2868 .ai-step-text { 2869 font-size: 12px; 2870 } 2871 2872 .suggest-item { 2873 padding: 8px 12px; 2874 } 2875 2876 .suggest-thumb { 2877 width: 36px; 2878 height: 36px; 2879 } 2880 2881 .suggest-name { 2882 font-size: 13px; 2883 } 2884 2885 .suggest-price { 2886 font-size: 11px; 2887 } 2888} 2889 2890@media (max-width: 480px) { 2891 .ai-bot-toggle { 2892 padding: 4px; 2893 } 2894 2895 .ai-bot-toggle .bot-icon { 2896 width: 18px; 2897 height: 18px; 2898 } 2899 2900 .search-icon-btn svg { 2901 width: 16px; 2902 height: 16px; 2903 } 2904 2905 /* Ultra-compact hints for small screens */ 2906 .suggest-header-hint { 2907 display: none; /* Hide header hint on very small screens */ 2908 } 2909 2910 .suggest-footer-hint { 2911 padding: 8px 10px; 2912 font-size: 10px; 2913 flex-direction: column; 2914 gap: 4px; 2915 } 2916 2917 .suggest-footer-hint svg { 2918 display: none; /* Hide icon to save space */ 2919 } 2920 2921 .suggest-footer-hint kbd { 2922 padding: 2px 4px; 2923 font-size: 9px; 2924 } 2925 2926 .ai-progress-steps { 2927 gap: 8px; 2928 } 2929 2930 .ai-step-text { 2931 font-size: 11px; 2932 } 2933} 2934 2935/* AI Search Prompt Box */ 2936.ai-search-prompt { 2937 padding: 24px; 2938 background: linear-gradient(135deg, #FFFBEB 0%, #ffffff 100%); 2939} 2940 2941.ai-prompt-header { 2942 display: flex; 2943 align-items: center; 2944 gap: 12px; 2945 margin-bottom: 16px; 2946} 2947 2948.ai-prompt-icon { 2949 width: 48px; 2950 height: 48px; 2951 background: linear-gradient(135deg, #FBBF24 0%, #CA8A04 100%); 2952 border-radius: 12px; 2953 display: flex; 2954 align-items: center; 2955 justify-content: center; 2956 font-size: 24px; 2957 box-shadow: 0 4px 12px rgba(251, 191, 36, 0.3); 2958} 2959 2960.ai-prompt-text { 2961 flex: 1; 2962} 2963 2964.ai-prompt-title { 2965 font-size: 16px; 2966 font-weight: 700; 2967 color: #1f2937; 2968 margin-bottom: 4px; 2969} 2970 2971.ai-prompt-subtitle { 2972 font-size: 13px; 2973 color: #6b7280; 2974} 2975 2976.ai-prompt-query { 2977 background: white; 2978 border: 2px solid #FBBF24; 2979 border-radius: 8px; 2980 padding: 12px 16px; 2981 margin-bottom: 16px; 2982} 2983 2984.ai-query-label { 2985 font-size: 11px; 2986 font-weight: 600; 2987 color: #FBBF24; 2988 text-transform: uppercase; 2989 letter-spacing: 0.5px; 2990 margin-bottom: 4px; 2991} 2992 2993.ai-query-text { 2994 font-size: 15px; 2995 font-weight: 600; 2996 color: #1f2937; 2997} 2998 2999.ai-search-trigger-btn { 3000 width: 100%; 3001 padding: 14px 20px; 3002 background: linear-gradient(135deg, #FBBF24 0%, #CA8A04 100%); 3003 color: white; 3004 border: none; 3005 border-radius: 10px; 3006 font-size: 15px; 3007 font-weight: 700; 3008 cursor: pointer; 3009 display: flex; 3010 align-items: center; 3011 justify-content: center; 3012 gap: 8px; 3013 transition: all 0.3s ease; 3014 box-shadow: 0 4px 12px rgba(251, 191, 36, 0.4); 3015} 3016 3017.ai-search-trigger-btn:hover { 3018 transform: translateY(-2px); 3019 box-shadow: 0 6px 20px rgba(251, 191, 36, 0.5); 3020} 3021 3022.ai-search-trigger-btn:active { 3023 transform: translateY(0); 3024} 3025 3026.ai-btn-icon { 3027 font-size: 18px; 3028 animation: sparkle 2s ease-in-out infinite; 3029} 3030 3031@keyframes sparkle { 3032 0%, 100% { opacity: 1; transform: scale(1); } 3033 50% { opacity: 0.7; transform: scale(1.2); } 3034} 3035 3036.ai-btn-text { 3037 font-size: 15px; 3038} 3039 3040.ai-prompt-footer { 3041 margin-top: 12px; 3042 text-align: center; 3043 font-size: 12px; 3044 color: #9ca3af; 3045 font-style: italic; 3046} 3047 3048/* ==================== TRENDING SEARCH STYLES ==================== */ 3049 3050.search-suggest.trending-mode { 3051 border-top: 3px solid #FBBF24; 3052 max-width: 900px; 3053 min-width: 600px; 3054} 3055 3056/* Two-column container */ 3057.trending-container { 3058 display: flex; 3059 gap: 0; 3060 padding: 0 !important; 3061} 3062 3063.trending-column { 3064 flex: 1; 3065 min-width: 0; 3066 padding: 0 !important; 3067} 3068 3069.trending-column.trending-searches { 3070 border-right: 2px solid #e2e8f0; padding: 0 !important; 3071} 3072 3073/* Mobile: stack columns vertically */ 3074@media (max-width: 768px) { 3075 .search-suggest.trending-mode { 3076 min-width: auto; 3077 max-width: 100%; 3078 } 3079 3080 .trending-container { 3081 flex-direction: column; 3082 } 3083 3084 .trending-column.trending-searches {
3085 border-right: none; 3086 border-bottom: 2px solid #e2e8f0; 3087 } 3088} 3089 3090.trending-header { 3091 display: flex; 3092 align-items: center; 3093 gap: 8px; 3094 padding: 14px 16px; 3095 border-bottom: 1px solid #e2e8f0; 3096 background: linear-gradient(135deg, #FBBF24 0%, #CA8A04 100%); 3097 color: white; 3098 position: sticky; 3099 top: 0; 3100 z-index: 10; 3101} 3102 3103.trending-icon { 3104 font-size: 20px; 3105 animation: trendingPulse 2s infinite; 3106} 3107 3108@keyframes trendingPulse { 3109 0%, 100% { 3110 transform: scale(1); 3111 filter: drop-shadow(0 0 0 rgba(251, 191, 36, 0)); 3112 } 3113 50% { 3114 transform: scale(1.15); 3115 filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8)); 3116 } 3117} 3118 3119.trending-title { 3120 font-weight: 700; 3121 font-size: 14px; 3122 letter-spacing: 0.3px; 3123} 3124 3125.trending-subtitle { 3126 font-size: 11px; 3127 opacity: 0.9; 3128 margin-left: auto; 3129 font-style: italic; 3130} 3131 3132/* Trending searches item */ 3133.trending-item { 3134 display: flex; 3135 align-items: center; 3136 gap: 12px; 3137 padding: 12px 16px; 3138 cursor: pointer; 3139 transition: all 0.2s ease; 3140 border-bottom: 1px solid #f1f5f9; 3141 position: relative; 3142} 3143 3144.trending-item:hover { 3145 background: linear-gradient(90deg, #FFFBEB 0%, #FEF3C7 100%); 3146 padding-left: 20px; 3147} 3148 3149.trending-item:hover::before { 3150 content: ''; 3151 position: absolute; 3152 left: 0; 3153 top: 0; 3154 bottom: 0; 3155 width: 4px; 3156 background: linear-gradient(180deg, #FBBF24 0%, #CA8A04 100%); 3157} 3158 3159.trending-item:last-child { 3160 border-bottom: none; 3161} 3162 3163.trending-rank { 3164 font-size: 20px; 3165 flex-shrink: 0; 3166 width: 32px; 3167 text-align: center; 3168} 3169 3170.trending-query { 3171 flex: 1; 3172 font-size: 14px; 3173 color: #1f2937; 3174 font-weight: 500; 3175 line-height: 1.4; 3176} 3177 3178.trending-category { 3179 font-size: 11px; 3180 color: #CA8A04; 3181 background: #FFFBEB; 3182 padding: 3px 10px; 3183 border-radius: 12px; 3184 white-space: nowrap; 3185 font-weight: 500; 3186 border: 1px solid #FDE68A; 3187} 3188 3189/* Trending products item */ 3190.trending-product-item { 3191 display: flex; 3192 align-items: center; 3193 gap: 12px; 3194 padding: 12px 16px; 3195 cursor: pointer; 3196 transition: all 0.2s ease; 3197 border-bottom: 1px solid #f1f5f9; 3198 position: relative; 3199} 3200 3201.trending-product-item:hover { 3202 background: linear-gradient(90deg, #FEF3C7 0%, #FDE68A 100%); 3203 padding-left: 20px; 3204} 3205 3206.trending-product-item:hover::before { 3207 content: ''; 3208 position: absolute; 3209 left: 0; 3210 top: 0; 3211 bottom: 0; 3212 width: 4px; 3213 background: linear-gradient(180deg, #EAB308 0%, #CA8A04 100%); 3214} 3215 3216.trending-product-item:last-child { 3217 border-bottom: none; 3218} 3219 3220.trending-product-thumb { 3221 width: 48px; 3222 height: 48px; 3223 object-fit: cover; 3224 border-radius: 8px; 3225 border: 1px solid #e5e7eb; 3226 flex-shrink: 0; 3227 background: white; 3228} 3229 3230.trending-product-info { 3231 flex: 1; 3232 min-width: 0; 3233} 3234 3235.trending-product-name { 3236 font-size: 13px; 3237 font-weight: 400; 3238 color: #1f2937; 3239 line-height: 1.4; 3240 margin-bottom: 0; 3241 padding: 0 !important; 3242 overflow: hidden; 3243 text-overflow: ellipsis; 3244 display: -webkit-box; 3245 -webkit-line-clamp: 2; 3246 -webkit-box-orient: vertical; 3247} 3248 3249.trending-product-price { 3250 font-size: 12px; 3251 font-weight: 700; 3252 color: #CA8A04; 3253 padding: 4px 0 !important; 3254} 3255.trending-product { padding: 0;} 3256/* ==================== END TRENDING STYLES ==================== */ 3257 3258</style> 3259 <style data-inlined="product-detail.css"> 3260/* === PRODUCT DETAIL v3 (Tokopedia-like) â Full stylesheet === */ 3261 3262/* Root / Container */ 3263.product-single { 3264 --pd-gap: 2rem; 3265 --pd-border: #e2e8f0; 3266 --pd-bg: #ffffff; 3267 --pd-muted: #64748b; 3268 --pd-dark: #0f172a; 3269 --pd-accent: #0d6efd; /* tema utama situs */ 3270 --pd-radius: 12px; 3271 --pd-soft: #f8fafc; 3272 --pd-soft-border: #e5e7eb; 3273 --pd-danger: #dc2626; 3274 --pd-success-bg: #dcfce7; 3275 --pd-success-text: #166534; 3276 --pd-danger-bg: #fee2e2; 3277 --pd-danger-text: #b91c1c; 3278 --pd-shadow: 0 6px 20px -10px rgba(0,0,0,.12); 3279 --pd-transition: .25s cubic-bezier(.4,.0,.2,1); 3280 max-width:1280px; 3281 margin-inline:auto; 3282} 3283 3284/* === Sticky Product Header Bar (muncul saat scroll) === */ 3285.pd-sticky-header { 3286 position: fixed; 3287 top: 0; 3288 left: 0; 3289 right: 0; 3290 z-index: 998; 3291 background: rgba(255, 255, 255, 0.98); 3292 backdrop-filter: blur(12px); 3293 -webkit-backdrop-filter: blur(12px); 3294 border-bottom: 1px solid rgba(226, 232, 240, 0.8); 3295 box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.08); 3296 transform: translateY(-100%); 3297 opacity: 0; 3298 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), 3299 opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); 3300 pointer-events: none; 3301} 3302 3303.pd-sticky-header.visible { 3304 transform: translateY(0); 3305 opacity: 1;
3306 pointer-events: auto; 3307} 3308 3309.pd-sticky-header-container { 3310 max-width: 1280px; 3311 margin: 0 auto; 3312 padding: 12px 20px; 3313 display: flex; 3314 align-items: center; 3315 gap: 16px; 3316} 3317 3318.pd-sticky-thumb { 3319 width: 48px; 3320 height: 48px; 3321 flex-shrink: 0; 3322 border-radius: 8px; 3323 border: 1px solid #e2e8f0; 3324 overflow: hidden; 3325 background: #f8fafc; 3326} 3327 3328.pd-sticky-thumb img { 3329 width: 100%; 3330 height: 100%; 3331 object-fit: cover; 3332 display: block; 3333} 3334 3335.pd-sticky-info { 3336 flex: 1; 3337 min-width: 0; 3338 display: flex; 3339 flex-direction: column; 3340 gap: 4px; 3341} 3342 3343.pd-sticky-title { 3344 font-size: 0.9rem; 3345 font-weight: 600; 3346 color: #0f172a; 3347 line-height: 1.3; 3348 white-space: nowrap; 3349 overflow: hidden; 3350 text-overflow: ellipsis; 3351 margin: 0; 3352} 3353 3354.pd-sticky-price-row { 3355 display: flex; 3356 align-items: center; 3357 gap: 8px; 3358 flex-wrap: wrap; 3359} 3360 3361.pd-sticky-price { 3362 font-size: 1.1rem; 3363 font-weight: 700; 3364 color: #0d6efd; 3365 line-height: 1; 3366} 3367 3368.pd-sticky-price-original { 3369 font-size: 0.75rem; 3370 color: #94a3b8; 3371 text-decoration: line-through; 3372 line-height: 1; 3373} 3374 3375.pd-sticky-discount { 3376 background: #dc2626; 3377 color: #fff; 3378 font-size: 0.65rem; 3379 font-weight: 600; 3380 padding: 2px 6px; 3381 border-radius: 4px; 3382 letter-spacing: 0.3px; 3383 line-height: 1; 3384} 3385 3386.pd-sticky-actions { 3387 display: flex; 3388 gap: 8px; 3389 flex-shrink: 0; 3390 align-items: center; 3391} 3392 3393.pd-sticky-share-btn { 3394 padding: 8px 16px; 3395 border-radius: 8px; 3396 border: 1px solid #e2e8f0; 3397 background: #fff; 3398 color: #64748b; 3399 cursor: pointer; 3400 transition: all 0.2s ease; 3401 display: inline-flex; 3402 align-items: center; 3403 justify-content: center; 3404 flex-shrink: 0; 3405} 3406 3407.pd-sticky-share-btn:hover { 3408 background: #f8fafc; 3409 border-color: #cbd5e1; 3410 color: #0f172a; 3411 transform: translateY(-1px); 3412} 3413 3414.pd-sticky-share-btn:active { 3415 transform: translateY(0); 3416} 3417 3418.pd-sticky-share-btn svg { 3419 display: block; 3420 width: 16px; 3421 height: 16px; 3422} 3423 3424.pd-sticky-btn-chat svg { 3425 width: 16px !important; 3426 height: 16px !important; 3427 max-width: none !important; 3428 margin-right: 4px; 3429 flex-shrink: 0; 3430} 3431 3432.pd-sticky-btn { 3433 padding: 8px 16px; 3434 border-radius: 8px; 3435 font-size: 0.85rem; 3436 font-weight: 600; 3437 cursor: pointer; 3438 transition: all 0.2s ease; 3439 border: 0; 3440 display: inline-flex; 3441 align-items: center; 3442 justify-content: center; 3443 white-space: nowrap; 3444} 3445 3446.pd-sticky-btn-cart { 3447 background: #fff; 3448 color: #0d6efd; 3449 border: 1px solid #0d6efd; 3450} 3451 3452.pd-sticky-btn-cart:hover { 3453 background: rgba(13, 110, 253, 0.06); 3454} 3455 3456.pd-sticky-btn-chat { 3457 background: #fff; 3458 color: #64748b; 3459 border: 1px solid #cbd5e1; 3460 flex: 0.8; 3461} 3462 3463.pd-sticky-btn-chat:hover { 3464 background: #f8fafc; 3465 border-color: #94a3b8; 3466 color: #475569; 3467} 3468 3469.pd-sticky-btn-buy { 3470 background: #0d6efd; 3471 color: #fff; 3472 border: 1px solid #0d6efd; 3473 box-shadow: 0 2px 8px -2px rgba(13, 110, 253, 0.4); 3474} 3475 3476.pd-sticky-btn-buy:hover { 3477 filter: brightness(0.95); 3478} 3479 3480.pd-sticky-btn:disabled { 3481 opacity: 0.5; 3482 cursor: not-allowed; 3483} 3484 3485/* Hide on mobile (use existing FAB instead) */ 3486@media (max-width: 880px) { 3487 .pd-sticky-header { 3488 display: none; 3489 } 3490} 3491 3492/* === PRINT STYLES (for PDF Export) === */ 3493@media print { 3494 /* Reset & Base */ 3495 * { 3496 -webkit-print-color-adjust: exact !important; 3497 print-color-adjust: exact !important; 3498 } 3499 3500 body { 3501 background: white !important; 3502 margin: 0; 3503 padding: 0; 3504 } 3505 3506 /* Hide all navigation, buttons, and interactive elements */ 3507 nav, header, footer, 3508 nav *, header *, footer *, 3509 .navbar, .navbar *, 3510 .nav-item, .nav-link, 3511 .pd-sticky-header, 3512 .pd-sticky-header.visible, 3513 .pd-breadcrumb, 3514 .pd-sidebar, 3515 .pd-mobile-fab, 3516 .pd-share-btn, 3517 .pd-sticky-share-btn, 3518 #pd-share-btn, 3519 .share-button, 3520 [data-share-btn], 3521 button, 3522 .btn-add-cart, 3523 .btn-buy, 3524 .pd-actions, 3525 .pd-compare-section, 3526 .pd-compare-btn, 3527 .pd-compare-info, 3528 #pd-compare-btn, 3529 .compare-note, 3530 .compare-container, 3531 .compare-dropdown, 3532 .compare-panel, 3533 .compare-wrapper, 3534 [data-compare], 3535 [id*="compare"], 3536 [class*="compare"], 3537 .pd-thumb-gallery, 3538 .pd-installment-card, 3539 .pd-installment-card2, 3540 .inst-alt-banner--compact, 3541 .qoala-banner-card, 3542 .tradein-compact-card, 3543 .pwp-deals-section, 3544 #pwp-deals-section, 3545 .pwp-trigger-badge, 3546 .pd-related-full, 3547 #pd-similar, 3548 .pd-similar-strip, 3549 .pd-scroll-top, 3550 .stock-info-btn, 3551 .stock-popover, 3552 .pd-countdown-modern, 3553 .pd-share-modal-overlay, 3554 [id*="promo-"], 3555 .bottom-nav, 3556 .pd-buy-form, 3557 .pd-buy-card, 3558 .pd-extra-meta, 3559 .pd-reviews-section, 3560 #reviews, 3561 hr, 3562 .divider, 3563 .separator { 3564 display: none !important; 3565 visibility: hidden !important; 3566 opacity: 0 !important; 3567 height: 0 !important; 3568 max-height: 0 !important; 3569 overflow: hidden !important; 3570 margin: 0 !important; 3571 padding: 0 !important; 3572 border: none !important; 3573 } 3574 3575 /* Extra aggressive reset for any remaining borders */ 3576 .pd-info-card *, 3577 .pd-meta-inline *, 3578 .pd-variants * { 3579 border: none !important; 3580 border-top: none !important; 3581 border-bottom: none !important; 3582 } 3583 3584 /* Layout restructure */ 3585 .product-single { 3586 max-width: 100% !important; 3587 margin: 0 !important; 3588 padding: 0 !important; 3589 box-shadow: none !important; 3590 } 3591 3592 /* Reset all shadows and borders for print */ 3593 * { 3594 box-shadow: none !important; 3595 } 3596 3597 .pd-top-layout { 3598 display: grid !important; 3599 grid-template-columns: 280px 1fr !important; 3600 gap: 24px !important; 3601 margin: 0 0 24px 0 !important; 3602 padding: 0 !important; 3603 page-break-inside: avoid;
3604 align-items: start !important; 3605 } 3606 3607 /* Print header with logo - centered and larger */ 3608 .product-single::before { 3609 content: "" !important; 3610 display: block !important; 3611 width: 150px !important; 3612 height: 50px !important; 3613 background: url('/assets//img/logo.png') no-repeat center !important; 3614 background-size: contain !important; 3615 margin: 0 auto 20px !important; 3616 visibility: visible !important; 3617 opacity: 1 !important; 3618 } 3619 3620 /* Media card - left column */ 3621 .pd-media-card { 3622 border: none !important; 3623 padding: 0 !important; 3624 margin: 0 !important; 3625 text-align: center; 3626 page-break-inside: avoid; 3627 } 3628 3629 /* Hide desktop image, use mobile slider for both desktop and mobile */ 3630 .pd-main-image { 3631 display: none !important; 3632 } 3633 3634 /* Mobile slider - show first image only in print */ 3635 .pd-mobile-slider { 3636 display: block !important; 3637 overflow: visible !important; 3638 } 3639 3640 .pd-mslide { 3641 display: none !important; 3642 } 3643 3644 .pd-mslide:first-child { 3645 display: block !important; 3646 border: 1px solid #e2e8f0 !important; 3647 max-width: 280px !important; 3648 margin: 0 auto !important; 3649 border-radius: 8px !important; 3650 } 3651 3652 .pd-mslide:first-child img { 3653 max-height: 280px !important; 3654 width: 100% !important; 3655 height: auto !important; 3656 object-fit: contain !important; 3657 } 3658 3659 /* Info card - right column */ 3660 .pd-info-card { 3661 border: none !important; 3662 border-top: none !important; 3663 border-bottom: none !important; 3664 padding: 0 !important; 3665 margin: 0 !important; 3666 background: white !important; 3667 box-shadow: none !important; 3668 } 3669 3670 /* Title */ 3671 .pd-title-row { 3672 display: block !important; 3673 margin-bottom: 10px !important; 3674 border: none !important; 3675 border-top: none !important; 3676 border-bottom: none !important; 3677 padding: 0 !important; 3678 box-shadow: none !important; 3679 text-decoration: none !important; 3680 } 3681 3682 /* Hide share button in print */ 3683 .pd-title-row .pd-share-btn, 3684 .pd-title-row #pd-share-btn { 3685 display: none !important; 3686 } 3687 3688 .pd-title { 3689 font-size: 13pt !important; 3690 color: #000 !important; 3691 margin: 0 0 10px 0 !important; 3692 page-break-after: avoid; 3693 font-weight: 700 !important; 3694 line-height: 1.3 !important; 3695 border: none !important; 3696 border-top: none !important; 3697 border-bottom: none !important; 3698 padding-bottom: 0 !important; 3699 box-shadow: none !important; 3700 text-decoration: none !important; 3701 } 3702 3703 /* Price and QR container - side by side */ 3704 .pd-price-qr-container { 3705 display: flex !important; 3706 gap: 16px !important; 3707 align-items: flex-start !important; 3708 margin-bottom: 14px !important; 3709 box-shadow: none !important; 3710 border: none !important; 3711 text-decoration: none !important; 3712 } 3713 3714 /* Price block - left side */ 3715 .pd-price-block { 3716 flex: 1 !important; 3717 margin: 0 !important; 3718 padding-bottom: 0 !important; 3719 border-bottom: none !important; 3720 box-shadow: none !important; 3721 } 3722 3723 .pd-price-row { 3724 margin-bottom: 8px !important; 3725 border: none !important; 3726 border-top: none !important; 3727 border-bottom: none !important; 3728 padding: 0 !important; 3729 } 3730 3731 /* Hide all empty or unnecessary elements in price row */ 3732 .pd-price-row > span:empty, 3733 .pd-price-row > .sold-inline, 3734 #pd-sold-inline { 3735 display: none !important; 3736 } 3737 3738 .pd-price-current { 3739 font-size: 18pt !important; 3740 color: #0d6efd !important; 3741 } 3742 3743 /* Show price range for variant products in print */ 3744 .pd-price-current[data-price-range]::after { 3745 content: attr(data-price-range) !important; 3746 display: block !important; 3747 font-size: 11pt !important; 3748 color: #64748b !important; 3749 margin-top: 4px !important; 3750 font-weight: normal !important; 3751 } 3752 3753 /* Hide 'Range:' prefix text */ 3754 .pd-price-current[data-price-range]::after { 3755 content: attr(data-price-range) !important; 3756 } 3757 3758 .pd-price-original { 3759 font-size: 12pt !important; 3760 color: #64748b !important; 3761 } 3762 3763 .pd-discount-badge { 3764 font-size: 10pt !important; 3765 padding: 2px 6px !important; 3766 } 3767 3768 /* QR Code section - right side beside price */ 3769 .pd-qr-print { 3770 display: block !important; 3771 flex: 0 0 100px !important; 3772 margin: 0 !important; 3773 padding: 8px !important; 3774 background: #f8fafc !important; 3775 border-radius: 8px !important; 3776 text-align: center !important; 3777 page-break-inside: avoid !important; 3778 border: 1px solid #cbd5e1 !important; 3779 } 3780 3781 .pd-qr-label { 3782 font-size: 6.5pt !important; 3783 color: #64748b !important; 3784 margin-bottom: 5px !important; 3785 line-height: 1.2 !important; 3786 font-weight: 600 !important; 3787 } 3788 3789 .pd-qr-image { 3790 width: 80px !important; 3791 height: 80px !important; 3792 display: block !important; 3793 margin: 0 auto !important; 3794 } 3795 3796 /* Meta info */ 3797 .pd-meta-inline { 3798 display: flex !important; 3799 gap: 12px !important; 3800 margin-bottom: 0 !important; 3801 padding-bottom: 14px !important; 3802 font-size: 10pt !important; 3803 border: none !important; 3804 border-top: none !important; 3805 border-bottom: none !important; 3806 padding-top: 0 !important; 3807 box-shadow: none !important; 3808 text-decoration: none !important; 3809 background: transparent !important; 3810 } 3811 3812 /* Hide stock badge in print */ 3813 .stock-badge, 3814 .stock-info-btn { 3815 display: none !important; 3816 } 3817 3818 .sku { 3819 color: #64748b !important; 3820 } 3821 3822 /* Hide sold badge if empty/zero */ 3823 .sold-inline { 3824 display: none !important; 3825 } 3826 3827 /* Hide variant summary section in print */ 3828 .pd-variant-combined, 3829 #pd-variant-combined, 3830 .pd-variant-summary, 3831 [data-role="sold-chip"] { 3832 display: none !important; 3833 } 3834 3835 /* Hide compare section completely in print */ 3836 .pd-compare-section, 3837 .pd-compare-btn, 3838 #pd-compare-btn, 3839 .pd-compare-info, 3840 .compare-note, 3841 .compare-text, 3842 .compare-remove { 3843 display: none !important; 3844 visibility: hidden !important; 3845 height: 0 !important; 3846 margin: 0 !important; 3847 padding: 0 !important; 3848 overflow: hidden !important; 3849 } 3850 3851 /* Variants - show all options with selected highlighted */ 3852 .pd-variants { 3853 margin: 0 !important; 3854 padding: 0 !important; 3855 page-break-inside: avoid !important; 3856 border: none !important; 3857 border-top: none !important; 3858 border-bottom: none !important; 3859 box-shadow: none !important; 3860 background: transparent !important; 3861 } 3862 3863 /* Add 'Variant:' label before variant groups */ 3864 .pd-variants::before { 3865 content: "Variant:" !important; 3866 display: block !important; 3867 font-size: 11pt !important; 3868 font-weight: 700 !important; 3869 color: #0f172a !important; 3870 margin-bottom: 10px !important; 3871 border: none !important; 3872 padding: 0 !important; 3873 border-top: none !important; 3874 border-bottom: none !important; 3875 } 3876 3877 .v-group { 3878 display: flex !important;
3879 align-items: center !important; 3880 gap: 8px !important; 3881 margin-bottom: 8px !important; 3882 line-height: 1 !important; 3883 page-break-inside: avoid !important; 3884 border: none !important; 3885 padding: 0 !important; 3886 } 3887 3888 .v-group:last-child { 3889 margin-bottom: 0 !important; 3890 } 3891 3892 .v-label { 3893 font-weight: 700 !important; 3894 margin: 0 !important; 3895 font-size: 9pt !important; 3896 color: #475569 !important; 3897 min-width: 60px !important; 3898 flex-shrink: 0 !important; 3899 border: none !important; 3900 padding: 0 !important; 3901 } 3902 3903 .v-options { 3904 display: flex !important; 3905 gap: 6px !important; 3906 flex-wrap: wrap !important; 3907 border: none !important; 3908 padding: 0 !important; 3909 margin: 0 !important; 3910 } 3911 3912 .v-chip { 3913 border: 1px solid #cbd5e1 !important; 3914 padding: 3px 10px !important; 3915 font-size: 9pt !important; 3916 background: #f8fafc !important; 3917 border-radius: 6px !important; 3918 display: inline-flex !important; 3919 align-items: center !important; 3920 page-break-inside: avoid !important; 3921 } 3922 3923 /* Highlight selected variant with gold background */ 3924 .v-chip.selected { 3925 background: #FFFBEB !important; 3926 border-color: #CA8A04 !important; 3927 font-weight: 600 !important; 3928 } 3929 3930 /* Hide radio inputs */ 3931 .v-chip input[type="radio"] { 3932 display: none !important; 3933 } 3934 3935 /* Hide "Habis" badge in print */ 3936 .v-chip .chip-badge { 3937 display: none !important; 3938 } 3939 3940 /* Short description - HIDDEN in print */ 3941 .pd-short-desc { 3942 display: none !important; 3943 } 3944 3945 /* QR Code section - right side beside price */ 3946 .pd-qr-print { 3947 display: block !important; 3948 flex: 0 0 100px !important; 3949 margin: 0 !important; 3950 padding: 8px !important; 3951 background: #f8fafc !important; 3952 border-radius: 8px !important; 3953 text-align: center !important; 3954 page-break-inside: avoid !important; 3955 border: 1px solid #cbd5e1 !important; 3956 } 3957 3958 .pd-qr-label { 3959 font-size: 6.5pt !important; 3960 color: #64748b !important; 3961 margin-bottom: 5px !important; 3962 line-height: 1.2 !important; 3963 font-weight: 600 !important; 3964 } 3965 3966 .pd-qr-image { 3967 width: 80px !important; 3968 height: 80px !important; 3969 display: block !important; 3970 margin: 0 auto !important; 3971 } 3972 3973 /* Full description - below 2 column layout */ 3974 .pd-full-description { 3975 border: none !important; 3976 padding: 0 !important; 3977 margin: 24px 0 0 0 !important; 3978 page-break-before: avoid; 3979 grid-column: 1 / -1 !important; 3980 } 3981 3982 .pd-full-desc-title { 3983 font-size: 13pt !important; 3984 font-weight: 700 !important; 3985 margin: 0 0 12px 0 !important; 3986 color: #000 !important; 3987 border-top: none !important; 3988 border-bottom: none !important; 3989 padding-top: 16px !important; 3990 } 3991 3992 .pd-full-desc-body { 3993 font-size: 10pt !important; 3994 line-height: 1.6 !important; 3995 color: #334155 !important; 3996 } 3997 3998 .pd-full-desc-body img { 3999 max-height: 200px !important; 4000 width: auto !important; 4001 page-break-inside: avoid; 4002 display: none !important; /* Hide images in print to save space */ 4003 } 4004 4005 /* Also target #deskripsi img if exists */ 4006 #deskripsi img { 4007 max-width: 350px !important; 4008 height: auto !important; 4009 } 4010 4011 .pd-full-desc-body table { 4012 border-collapse: collapse !important; 4013 width: 100% !important; 4014 margin: 12px 0 !important; 4015 font-size: 9pt !important; 4016 } 4017 4018 .pd-full-desc-body th, 4019 .pd-full-desc-body td { 4020 border: 1px solid #cbd5e1 !important; 4021 padding: 6px 8px !important; 4022 } 4023 4024 /* Print footer */ 4025 .product-single::after { 4026 content: "Plaza IT - plazait.co.id | Printed: " attr(data-print-date); 4027 display: block; 4028 margin-top: 30px; 4029 padding-top: 16px; 4030 border-top: 1px solid #e2e8f0; 4031 font-size: 9pt; 4032 color: #64748b; 4033 text-align: center; 4034 } 4035 4036 /* Page breaks */ 4037 .pd-media-card, 4038 .pd-price-block, 4039 .pd-full-desc-title { 4040 page-break-after: avoid; 4041 } 4042 4043 .pd-full-description { 4044 page-break-before: auto; 4045 } 4046} 4047 4048/* === Breadcrumb (truncate last item) === */ 4049.pd-breadcrumb{ 4050 display:flex; 4051 flex-wrap:nowrap; 4052 align-items:center; 4053 gap:.5rem; 4054 font-size:.72rem; 4055 margin:.7rem 0 1.2rem .5rem; 4056 color:var(--pd-muted); 4057 overflow:hidden; 4058} 4059.pd-breadcrumb a{color:var(--pd-accent);text-decoration:none} 4060.pd-breadcrumb a:hover{text-decoration:underline} 4061.pd-breadcrumb a, 4062.pd-breadcrumb span{flex:0 0 auto} 4063.pd-breadcrumb .current{ 4064 flex:1 1 auto; 4065 min-width:0; 4066 white-space:nowrap; 4067 overflow:hidden; 4068 text-overflow:ellipsis; 4069 font-weight:600;
4070 color:var(--pd-dark); 4071 padding-right:2px; 4072} 4073 4074/* === Top layout === */ 4075.pd-top-layout{ 4076 display:grid; 4077 gap:var(--pd-gap); 4078 grid-template-columns:minmax(260px,340px) minmax(460px,1fr) 300px; 4079 align-items:start; 4080 margin-bottom:2.4rem; 4081} 4082 4083/* === Media column === */ 4084.pd-media-card{ 4085 background:#fff; 4086 border:1px solid var(--pd-border); 4087 border-radius:var(--pd-radius); 4088 padding:1rem 1rem 1.2rem; 4089} 4090.pd-main-image{margin:0 0 .8rem;border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff;position:relative} 4091.pd-main-image img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4092 4093/* Gallery Thumbnails */ 4094.pd-thumb-gallery{margin-top:.6rem;display:flex;flex-wrap:wrap;gap:.55rem} 4095.pd-thumb-btn{ 4096 border:1px solid var(--pd-soft-border); 4097 background:#fff; 4098 cursor:pointer; 4099 width:64px;height:64px;padding:2px; 4100 display:flex;align-items:center;justify-content:center; 4101 overflow:hidden;border-radius:8px; 4102 transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease; 4103 position:relative; 4104} 4105.pd-thumb-btn img{max-width:100%;max-height:100%;object-fit:contain;display:block} 4106.pd-thumb-btn:hover{border-color:var(--pd-accent)} 4107.pd-thumb-btn.active{border-color:var(--pd-accent);box-shadow:0 0 0 2px rgba(13,110,253,.25)} 4108.pd-thumb-btn:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4109.pd-thumb-btn:active{transform:scale(.96)} 4110 4111/* === Info column === */ 4112.pd-info-card{ 4113 background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4114 padding:1.4rem 1.6rem 2rem; 4115} 4116.pd-title{margin:0 0 1rem;font-size:1.2rem;line-height:1.25;font-weight:600;color:var(--pd-dark)} 4117.pd-price-block{margin:0 0 1rem} 4118.pd-price-row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;margin:0 0 .35rem} 4119.pd-price-current{font-size:2rem;font-weight:700;color:var(--pd-accent)} 4120.pd-price-original{font-size:.9rem;text-decoration:line-through;color:#94a3b8} 4121.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} 4122.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} 4123.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} 4124.stock-badge.in{background:var(--pd-success-bg);color:var(--pd-success-text)} 4125.stock-badge.out{background:var(--pd-danger-bg);color:var(--pd-danger-text)} 4126.digital-badge{padding:4px 10px;border-radius:24px;background:linear-gradient(135deg,#C9A227 0%,#8B6914 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} 4127.sku{color:var(--pd-muted)} 4128.pd-short-desc{font-size:.85rem;line-height:1.5;margin:0 0 1.2rem;color:#334155;overflow-wrap:anywhere} 4129.pd-short-desc p{margin:0 0 .75rem} 4130.pd-short-desc ul,.pd-short-desc ol{margin:.5rem 0 .9rem 1.1rem;font-size:.85rem;line-height:1.45} 4131 4132/* Meta row for sold and rating badges */ 4133.pd-meta-row { 4134 display: flex; 4135 align-items: center; 4136 flex-wrap: wrap; 4137 gap: 5px; 4138 margin-top: 8px; 4139 margin-bottom: 4px; 4140} 4141 4142/* Sales (Terjual) badge inline */ 4143.sold-inline{ 4144 display:inline-block;font-size:.65rem;font-weight:600;color:#475569; 4145 background:#f1f5f9;padding:3px 8px 4px;border-radius:6px;letter-spacing:.4px;margin-left:0; 4146} 4147 4148/* Price row divider */ 4149.price-divider { 4150 display: inline-block; 4151 margin: 0 .4rem; 4152 color: #cbd5e1; 4153 font-weight: 400; 4154} 4155 4156/* Rating badge inline */ 4157.rating-inline { 4158 display: inline-flex; 4159 align-items: center; 4160 gap: 4px; 4161 padding: 3px 8px 4px; 4162 background: #FFFBEB; 4163 border-radius: 6px; 4164 font-size: .65rem; 4165 font-weight: 600; 4166 color: #CA8A04; 4167 text-decoration: none; 4168 transition: all .2s ease; 4169 cursor: pointer; 4170 margin-left: .5rem; 4171 white-space: nowrap; 4172} 4173 4174.rating-inline:hover { 4175 background: #FEF3C7; 4176 transform: translateY(-1px); 4177 box-shadow: 0 2px 4px rgba(234, 88, 12, 0.1); 4178} 4179 4180.rating-inline .star-icon { 4181 font-size: .75rem; 4182 color: #CA8A04; 4183 line-height: 1; 4184} 4185 4186.rating-inline .rating-value { 4187 color: #CA8A04; 4188} 4189 4190.rating-inline .rating-badge-count { 4191 color: #78350F; 4192 opacity: 0.85; 4193} 4194 4195@media (max-width: 640px) { 4196 .rating-inline { 4197 font-size: .65rem; 4198 padding: 3px 8px 4px; 4199 } 4200 .rating-inline .star-icon { 4201 font-size: .7rem; 4202 } 4203} 4204 4205/* Highlight animation for review section */ 4206@keyframes highlightFlash { 4207 0%, 100% { background-color: transparent; } 4208 50% { background-color: rgba(251, 191, 36, 0.1); } 4209} 4210 4211.highlight-flash { 4212 animation: highlightFlash 1.5s ease; 4213} 4214 4215/* === Variant chips === */ 4216.pd-variants{margin-top:14px} 4217.v-group{margin-bottom:12px} 4218.v-label{font-weight:600;margin-bottom:6px} 4219.v-options{display:flex;flex-wrap:wrap;gap:8px} 4220.v-chip{ 4221 display:inline-flex;align-items:center;gap:8px; 4222 padding:6px 10px;border:1px solid #d6d9de;border-radius:999px; 4223 cursor:pointer;background:#fff;transition:.15s ease;user-select:none 4224} 4225.v-chip:hover{border-color:#9aa3af;box-shadow:0 1px 0 rgba(0,0,0,.04)} 4226.v-chip.selected{border-color:#CA8A04;background:#FFFBEB} 4227.v-chip.disabled{opacity:.45;cursor:not-allowed} 4228.v-chip.out{border-style:dashed} 4229.v-chip input{display:none} 4230.v-chip .dot{width:14px;
4230height:14px;border-radius:50%;background:var(--dot,#999);box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)} 4231.v-chip .dot--neutral{background:#9aa3af} 4232.v-chip .txt{font-size:.95rem;white-space:nowrap} 4233.v-chip .chip-badge{margin-left:2px;font-size:.72rem;line-height:1;padding:3px 6px;border-radius:6px;background:#fee2e2;color:#991b1b;border:1px solid #fecaca} 4234.pd-variant-summary{margin-top:8px;color:#475569;font-size:.92rem} 4235.pd-variant-summary .vs-item{display:inline-flex;gap:4px;margin-right:10px} 4236 4237/* === Buy form (di sidebar kanan untuk desktop) === */ 4238.pd-buy-form{display:flex;flex-wrap:wrap;align-items:flex-end;gap:1rem 1.4rem;margin:0} 4239.qty-label{display:block;font-size:.6rem;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--pd-muted);margin-bottom:.35rem} 4240.pd-buy-form input[type=number]{width:90px;padding:.55rem .6rem;border:1px solid var(--pd-soft-border);border-radius:6px;font-size:.85rem} 4241.pd-actions{display:flex;gap:.8rem;flex-wrap:wrap} 4242 4243/* Tombol senada tema */ 4244.btn-add-cart,.btn-buy,.btn-chat-icon{ 4245 cursor:pointer;border:0;font-weight:700;border-radius:10px;font-size:.9rem; 4246 display:inline-flex;align-items:center;justify-content:center;gap:.4rem; 4247 min-height:44px;padding:0 1rem;transition:var(--pd-transition) 4248} 4249.btn-add-cart{background:var(--brand-500);color:#fff;box-shadow:0 2px 8px -2px rgba(13,110,253,.35)} 4250.btn-add-cart:hover{filter:brightness(.95)} 4251.btn-buy{background:#fff;color:var(--brand-500);border:2px solid var(--brand-500)} 4252.btn-buy:hover{background:rgba(13,110,253,.06)} 4253.btn-chat-icon{background:#fff;color:#FBBF24;border:2px solid #FBBF24;padding:0;width:44px;min-width:44px} 4254.btn-chat-icon:hover{background:rgba(251, 191, 36, 0.06);border-color:#CA8A04} 4255.btn-chat-icon svg{width:20px;height:20px;flex-shrink:0} 4256.btn-add-cart:disabled,.btn-buy:disabled,.btn-chat-icon:disabled{opacity:.5;cursor:not-allowed} 4257 4258/* Buy with Voucher Button - Cleaner, more compact design */ 4259.btn-buy-voucher{ 4260 cursor:pointer;border:0;font-weight:600;border-radius:10px; 4261 display:inline-flex;align-items:center;justify-content:space-between;gap:10px; 4262 min-height:44px;padding:0 14px 0 12px;transition:all 0.3s ease; 4263 background:linear-gradient(135deg, #8B6914 0%, #B45309 100%); 4264 color:#fff; 4265 box-shadow:0 3px 10px rgba(124,58,237,0.3); 4266 position:relative; 4267 overflow:hidden; 4268 flex:1 1 auto 4269} 4270.btn-buy-voucher:hover{ 4271 transform:translateY(-2px); 4272 box-shadow:0 5px 16px rgba(124,58,237,0.4) 4273} 4274.btn-buy-voucher:active{ 4275 transform:translateY(0); 4276 box-shadow:0 2px 6px rgba(124,58,237,0.3) 4277} 4278.btn-buy-voucher .bv-icon{ 4279 font-size:20px; 4280 line-height:1; 4281 flex-shrink:0 4282} 4283.btn-buy-voucher .bv-text{ 4284 display:flex; 4285 flex-direction:column; 4286 align-items:flex-start; 4287 gap:2px; 4288 flex:1; 4289 min-width:0; 4290 font-size:14px; 4291 line-height:1.2 4292} 4293.btn-buy-voucher .bv-text small{ 4294 font-size:10px; 4295 opacity:0.85; 4296 font-weight:500; 4297 letter-spacing:0.3px; 4298 font-family:monospace 4299} 4300.btn-buy-voucher .bv-save{ 4301 font-size:12px; 4302 font-weight:700; 4303 background:rgba(255,255,255,0.25); 4304 padding:4px 8px; 4305 border-radius:6px; 4306 white-space:nowrap; 4307 flex-shrink:0 4308} 4309 4310/* Extra meta (opsional) */ 4311.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} 4312.pd-extra-meta span{font-weight:600;color:var(--pd-muted);margin-right:.3rem} 4313.pd-extra-meta a{color:var(--pd-accent);text-decoration:none} 4314.pd-extra-meta a:hover{text-decoration:underline} 4315 4316/* === Sidebar === */ 4317.pd-sidebar{display:flex;flex-direction:column;gap:1.2rem} 4318.pd-side-card{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1rem 1.1rem 1.2rem} 4319.pd-side-card h3{margin:0 0 .7rem;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4320.pd-side-card ul{list-style:none;margin:0;padding:0;display:grid;gap:.45rem;font-size:.7rem;color:#334155} 4321.pd-side-card li{position:relative;padding-left:13px;line-height:1.25} 4322.pd-side-card li:before{content:'';width:6px;height:6px;background:var(--pd-accent);border-radius:50%;position:absolute;
4322left:0;top:.45rem} 4323.pay-logos{display:flex;gap:.4rem;flex-wrap:wrap;margin:.1rem 0 .4rem} 4324.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} 4325 4326/* Buy card hanya desktop (mobile pakai FAB) */ 4327.pd-buy-card{display:block} 4328@media (max-width:880px){ .pd-buy-card{display:none} .pd-title-row {padding-right: 0 !important;}} 4329 4330/* === Full Description === */ 4331.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} 4332.pd-full-desc-title{margin:0 0 1.1rem;font-size:1.15rem;font-weight:600;color:var(--pd-dark)} 4333.pd-full-desc-body{font-size:.9rem;line-height:1.6;color:#334155;overflow-wrap:anywhere} 4334.pd-full-desc-body p{margin:0 0 1rem} 4335.pd-full-desc-body ul,.pd-full-desc-body ol{margin:.5rem 0 1rem 1.2rem} 4336.pd-full-desc-body table{width:100%;border-collapse:collapse;margin:1rem 0 1.2rem} 4337.pd-full-desc-body table,.pd-full-desc-body th,.pd-full-desc-body td{border-bottom:1px dotted #cbd5e1;vertical-align: top} 4338.pd-full-desc-body th,.pd-full-desc-body td{padding:.45rem .55rem;text-align:left} 4339.pd-full-desc-body img{max-width:100%;height:auto;display:block;} 4340 4341/* === Related (container) === */ 4342.pd-related-full{ 4343 background:#fff;border:1px solid var(--pd-border); 4344 border-radius:var(--pd-radius);padding:1.4rem 1.6rem 1.9rem; 4345 margin:0 0 2rem; 4346} 4347.pd-related-title{margin:0 0 1.1rem;font-size:1.05rem;font-weight:600;color:var(--pd-dark)} 4348 4349/* Related Grid (dirapikan: gabung selector agar tidak duplikat) */ 4350.pd-related-grid, 4351.pd-related-grid--full{ 4352 display:grid !important; 4353 gap:16px !important; 4354 grid-template-columns:repeat(auto-fill,minmax(170px,1fr)) !important; 4355 align-items:stretch; 4356} 4357 4358/* Fallback jika grid tidak didukung */ 4359@supports not (display:grid){ 4360 .pd-related-grid, 4361 .pd-related-grid--full{display:flex !important;flex-wrap:wrap} 4362 .pd-related-grid .pd-related-card, 4363 .pd-related-grid--full .pd-related-card{flex:0 0 calc(20% - 16px);max-width:calc(20% - 16px)} 4364 @media (max-width:1100px){ 4365 .pd-related-grid .pd-related-card, 4366 .pd-related-grid--full .pd-related-card{flex:0 0 calc(25% - 16px);max-width:calc(25% - 16px)} 4367 } 4368 @media (max-width:820px){ 4369 .pd-related-grid .pd-related-card, 4370 .pd-related-grid--full .pd-related-card{flex:0 0 calc(33.333% - 16px);max-width:calc(33.333% - 16px)} 4371 } 4372 @media (max-width:580px){ 4373 .pd-related-grid .pd-related-card, 4374 .pd-related-grid--full .pd-related-card{flex:0 0 calc(50% - 16px);max-width:calc(50% - 16px)} 4375 } 4376} 4377 4378/* Related Card */ 4379.pd-related-card{ 4380 display:flex;flex-direction:column;border:1px solid var(--pd-soft-border); 4381 background:#fff;border-radius:10px;text-decoration:none;color:inherit; 4382 overflow:hidden;transition:var(--pd-transition);position:relative;min-width:0; 4383} 4384.pd-related-card:hover{transform:translateY(-4px);box-shadow:var(--pd-shadow)} 4385.pd-related-card .thumb{aspect-ratio:1/1;background:#f1f5f9;overflow:hidden;position:relative} 4386.pd-related-card img{width:100%;height:100%;object-fit:cover;display:block} 4387.mini-disc{position:absolute;top:6px;left:6px;background:#dc2626;color:#fff;font-size:.55rem;padding:3px 6px 2px;border-radius:4px;font-weight:600} 4388.pd-related-card .info{padding:.55rem .65rem .75rem;display:flex;flex-direction:column;gap:.25rem} 4389.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} 4390.pd-related-card .price{font-size:.7rem;font-weight:600;color:var(--pd-accent)} 4391.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;} 4392.pd-related-card .price .final {font-size: .82rem; font-weight: 700; color: #0d6efd;} 4393.pd-related-card .price .ori {font-size: .6rem; text-decoration: line-through; color: #94a3b8;font-weight: 500;} 4394 4395/* === Mobile slider (scroll-snap) â gambar utama tetap tampil === */ 4396.pd-mobile-slider{display:none} 4397.pd-mobile-slider img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4398 4399/* Gallery counter badge */ 4400.pd-gallery-counter{ 4401 position:absolute; 4402 bottom:12px; 4403 left:12px; 4404 background:rgba(255,255,255,0.25); 4405 color:#9E9E9E; 4406 padding:4px 10px; 4407 border-radius:10px; 4408 font-size:12px; 4409 font-weight:600; 4410 z-index:100; 4411 backdrop-filter:blur(6px); 4412 -webkit-backdrop-filter:blur(6px); 4413 font-variant-numeric:tabular-nums; 4414 pointer-events:none; 4415 display:none; 4416 box-shadow:0 1px 4px rgba(0,0,0,.15); 4417 line-height:1; 4418 border:1px solid rgba(0,0,0,0.08); 4419} 4420 4421@media (max-width:640px){ 4422 .pd-media-card{border: 0;padding: 0 1rem 0;} 4423 .pd-info-card {border: 0; padding: 0 1.3rem 0; } 4424 .pd-side-card {flex: 1 1 !important;} 4425 .pd-top-layout {gap: 0; margin-bottom: 0;} 4426 .pd-variants {margin-bottom: 1.5rem;} 4427 .pd-main-image{display:none} 4428 .pd-gallery-counter{display:block} 4429 .pd-mobile-slider-wrapper{ 4430 position:relative; 4431 margin-bottom:.6rem; 4432 } 4433 .pd-mobile-slider{ 4434 display:flex;gap:8px;overflow-x:auto; 4435 scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
4436 } 4437 .pd-mslide{ 4438 min-width:100%;flex:0 0 100%;scroll-snap-align:center; 4439 border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff 4440 } 4441 .pd-thumb-gallery{display:none} /* hide thumbnails on mobile */ 4442} 4443 4444/* === Mobile Floating Action Bar === */ 4445.pd-mobile-fab{ 4446 position:fixed; 4447 left:0; 4448 right:0; 4449 bottom:42px; /* Position above bottom-nav (60px min-height + 6px padding) */ 4450 display:none; 4451 align-items:center; 4452 gap:.75rem; 4453 padding:12px 16px; 4454 background:rgba(255,255,255,0.95); 4455 backdrop-filter:blur(20px); 4456 -webkit-backdrop-filter:blur(20px); 4457 border-top:1px solid rgba(228,231,235,0.6); 4458 box-shadow:0 -4px 16px -4px rgba(0,0,0,0.12); 4459 z-index:999; /* Below bottom-nav (1000) */ 4460 transition:bottom .3s cubic-bezier(0.4, 0.0, 0.2, 1); 4461} 4462 4463/* When bottom-nav is hidden, move FAB to bottom */ 4464body:has(.bottom-nav.hidden) .pd-mobile-fab { 4465 bottom: 0; 4466} 4467.pd-mobile-fab .mf-actions{display:flex;gap:.5rem;width:100%} 4468.mf-btn{ 4469 border-radius:10px; 4470 padding:.6rem 1rem; 4471 font-weight:600; 4472 font-size:14px; 4473 cursor:pointer; 4474 transition:all .2s; 4475 -webkit-tap-highlight-color:transparent; 4476 display:inline-flex; 4477 align-items:center; 4478 justify-content:center; 4479 gap:4px; 4480} 4481.mf-buy{ 4482 background:#FBBF24; 4483 color:#fff; 4484 border:0; 4485 box-shadow:0 2px 8px rgba(251, 191, 36, 0.3); 4486 flex:1; 4487 flex-direction:column; 4488 padding:.55rem 1rem; 4489 line-height:1; 4490 min-height:44px; 4491 font-family:inherit; 4492} 4493.mf-buy .mf-buy-text{ 4494 font-size:11px; 4495 font-weight:600; 4496 opacity:0.95; 4497 font-family:inherit; 4498} 4499.mf-buy .mf-buy-price{ 4500 font-size:13px; 4501 font-weight:800; 4502 letter-spacing:-0.3px; 4503 font-family:inherit; 4504 line-height:1; 4505} 4506.mf-buy:active{transform:scale(0.97)} 4507.mf-cart{ 4508 background:#fff; 4509 color:#FBBF24; 4510 border:2px solid #FBBF24; 4511 width:44px; 4512 height:44px; 4513 min-height:44px; 4514 padding:0; 4515 flex-shrink:0; 4516 font-family:inherit; 4517} 4518.mf-cart:active{transform:scale(0.95)} 4519.mf-chat{background:#fff;color:#FBBF24;border:2px solid #FBBF24;width:44px;height:44px;min-height:44px;padding:0;flex-shrink:0;font-family:inherit} 4520.mf-chat:active{transform:scale(0.95)} 4521.mf-chat svg{width:20px;height:20px;flex-shrink:0} 4522.mf-share{ 4523 background:#fff; 4524 color:#FBBF24; 4525 border:2px solid #FBBF24; 4526 width:44px; 4527 height:44px; 4528 padding:0; 4529 flex-shrink:0; 4530} 4531.mf-share:active{transform:scale(0.95)} 4532.mf-btn:disabled{opacity:.5;cursor:not-allowed} 4533@media (max-width:640px){ 4534 .pd-mobile-fab{display:flex} 4535 .product-single{padding-bottom:0;} /* Space for FAB + bottom-nav */ 4536} 4537 4538/* === Installment cards === */ 4539.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} 4540.pd-installment-card h3{margin:0 0 .2rem;font-size:.78rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4541 4542.inst-tabs{display:flex;gap:.4rem} 4543.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)} 4544.inst-tab.active,.inst-tab:hover{background:var(--pd-accent);color:#fff;border-color:var(--pd-accent)} 4545.inst-tab:focus{outline:2px solid var(--pd-accent);outline-offset:2px} 4546 4547.inst-tenors{display:flex;gap:.4rem;flex-wrap:wrap} 4548.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} 4549.tenor-btn.active,.tenor-btn:hover{background:#1d7df0;color:#fff;border-color:#1d7df0} 4550 4551.inst-result{display:flex;flex-direction:column;gap:.45rem;font-size:.63rem} 4552.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} 4553.inst-line.highlight{background:#1d7df0;color:#fff;border-color:#1d7df0;font-weight:600} 4554.inst-line .lbl{opacity:.8} 4555.inst-line .val{font-weight:600;letter-spacing:.3px} 4556.inst-footnote{font-size:.53rem;line-height:1.25;color:#64748b;margin-top:.2rem} 4557@media (max-width: 860px){ 4558 .pd-installment-card{order:3} 4559 .inst-tabs .inst-tab,.inst-tenors .tenor-btn{font-size:.65rem} 4560 4561 /* Voucher button responsive - make it simpler */ 4562 .btn-buy-voucher{ 4563 padding:0 12px; 4564 gap:8px 4565 } 4566 .btn-buy-voucher .bv-icon{ 4567 font-size:18px 4568 } 4569 .btn-buy-voucher .bv-text{ 4570 font-size:13px 4571 } 4572 .btn-buy-voucher .bv-text small{ 4573 font-size:9px 4574 } 4575 .btn-buy-voucher .bv-save{ 4576 font-size:11px; 4577 padding:3px 6px 4578 } 4579} 4580 4581/* Installment List v2 (eye-catching) */ 4582.pd-installment-card2{ 4583 background:linear-gradient(135deg,#ffffff 0%,#f1f7ff 65%,#e8f2ff 100%); 4584 border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4585 padding:1rem 1.05rem 1.2rem;display:flex;flex-direction:column;gap:.5rem; 4586 position:relative;overflow:hidden 4587} 4588.pd-installment-card2:before{ 4589 content:'';position:absolute;width:140px;height:140px; 4590 background:radial-gradient(circle at center,rgba(29,125,240,.18),rgba(29,125,240,0)); 4591 top:-40px;right:-40px;pointer-events:none 4592} 4593.pd-installment-card2 h3{margin:0;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:#0f2f54} 4594.pd-installment-card2 .inst-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem} 4595.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)} 4596 4597.inst-bank-tabs{display:flex;gap:.45rem} 4598.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} 4599.bank-tab.active,.bank-tab:hover{background:#1d7df0;color:#fff;border-color:#1d7df0;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4600.bank-tab:focus{outline:2px solid #1d7df0;outline-offset:2px} 4601 4602.inst-month-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem} 4603.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} 4604.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} 4605.inst-month-item:hover{transform:translateY(-2px);box-shadow:0 6px 14px -6px rgba(0,40,120,.25)} 4606.inst-month-item:hover:before{opacity:1} 4607.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)} 4608.amount{flex:1;font-size:.8rem;font-weight:600;color:#0f2f54;letter-spacing:.3px} 4609.tag{font-size:.55rem;font-weight:600;padding:.28rem .55rem;border-radius:30px;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap} 4610.tag-zero{background:#16a34a;color:#fff;box-shadow:0 2px 8px -3px rgba(16,180,90,.55)} 4611 4612.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} 4613.inst-disclaimer{font-size:.52rem;line-height:1.25;color:#64748b;margin-top:-.2rem} 4614 4615@media (max-width:860px){ 4616 .pd-installment-card2{order:3} 4617 .tenor-badge{min-width:38px;font-size:.75rem} 4618 .amount{font-size:.78rem} 4619} 4620 4621/* Alt Fintech Banner (compact) */ 4622.inst-alt-banner--compact{ 4623 background:linear-gradient(142deg,#1d7df0 0%,#3c98ff 55%,#67bdff 100%); 4624 color:#fff;border:0;padding:.95rem .95rem 1.05rem;border-radius:16px; 4625 box-shadow:0 6px 18px -6px rgba(0,70,160,.45);position:relative;overflow:hidden;min-height:auto; 4626} 4627.inst-alt-banner--compact:before, 4628.inst-alt-banner--compact:after{ 4629 content:'';position:absolute;border-radius:50%;background:rgba(255,255,255,.15);filter:blur(2px);pointer-events:none; 4630} 4631.inst-alt-banner--compact:before{width:130px;height:130px;top:-50px;right:-35px} 4632.inst-alt-banner--compact:after{width:90px;height:90px;bottom:-40px;left:-30px}
4633.inst-alt-banner--compact .iab-top{margin:0 0 .55rem;display:flex;flex-direction:column;gap:.25rem} 4634.inst-alt-banner--compact .iab-title{font-size:.74rem;font-weight:700;letter-spacing:.7px;line-height:1.15} 4635.inst-alt-banner--compact .iab-sub{font-size:.55rem;font-weight:500;opacity:.95;letter-spacing:.4px} 4636 4637.iab-logos-compact{display:flex;gap:.55rem;align-items:center;margin:0 0 .6rem;flex-wrap:nowrap;justify-content:space-between} 4638.iab-logos-compact .logo-fin{width:30%} 4639.iab-logos-compact .logo{ 4640 width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center; 4641 font-size:.62rem;font-weight:700;letter-spacing:.3px;position:relative;color:#fff; 4642 box-shadow:0 4px 10px -4px rgba(0,0,0,.35);background:rgba(255,255,255,.15); 4643 backdrop-filter:blur(4px);transition:.25s;text-shadow:0 1px 2px rgba(0,0,0,.4);user-select:none; 4644} 4645.iab-logos-compact .logo:before{ 4646 content:'';position:absolute;inset:0;border-radius:inherit; 4647 background:linear-gradient(140deg,rgba(255,255,255,.35),rgba(255,255,255,0));mix-blend-mode:overlay;opacity:.55;pointer-events:none; 4648} 4649.iab-logos-compact .logo:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 8px 18px -6px rgba(0,0,0,.45)} 4650 4651.iab-cta--compact{ 4652 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; 4653} 4654.iab-cta--compact:hover{transform:translateY(-2px);box-shadow:0 8px 18px -7px rgba(0,0,0,.45)} 4655.iab-footnote--compact{margin-top:.45rem;font-size:.46rem;opacity:.85;letter-spacing:.25px} 4656@media (max-width:860px){ 4657 .inst-alt-banner--compact{order:4;margin-top:.4rem;flex:1 1} 4658 .iab-logos-compact .logo{width:46px;height:46px} 4659} 4660 4661/* === Scroll To Top (baru) === */ 4662.pd-scroll-top{ 4663 position:fixed; 4664 right:20px; 4665 bottom:90px; 4666 width:48px; 4667 height:48px; 4668 border-radius:50%; 4669 background:linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); 4670 color:#0f2f54; 4671 border:1px solid rgba(226, 232, 240, 0.8); 4672 box-shadow:0 8px 20px -8px rgba(0,0,0,.3), 0 2px 6px rgba(0,0,0,.1); 4673 display:flex; 4674 align-items:center; 4675 justify-content:center; 4676 cursor:pointer; 4677 z-index:1105; 4678 opacity:0; 4679 transform:translateY(12px) scale(.92); 4680 transition:all .3s cubic-bezier(0.4, 0, 0.2, 1); 4681 backdrop-filter:blur(8px); 4682 -webkit-backdrop-filter:blur(8px); 4683} 4684.pd-scroll-top:hover{ 4685 box-shadow:0 12px 28px -10px rgba(0,0,0,.4), 0 4px 12px rgba(0,0,0,.15); 4686 transform:translateY(0) scale(1); 4687 background:linear-gradient(135deg, #ffffff 0%, #fefefe 100%); 4688 border-color:rgba(226, 232, 240, 1); 4689} 4690.pd-scroll-top:active{ 4691 transform:translateY(0) scale(.96); 4692} 4693.pd-scroll-top:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4694.pd-scroll-top.visible{opacity:1;transform:translateY(0) scale(1)} 4695@media (max-width:640px){ 4696 /* Dynamic positioning via JavaScript */ 4697 .pd-scroll-top{ 4698 right:14px; 4699 bottom:var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))); 4700 } 4701} 4702 4703/* === Responsive layout tweaks === */ 4704@media (max-width:1250px){ 4705 .pd-top-layout{grid-template-columns:minmax(260px,330px) 1fr} 4706 .pd-sidebar{grid-column:1 / -1;flex-direction:row;flex-wrap:wrap;gap:1rem} 4707 /* Other cards 2-per-row; buy card always takes full row to avoid height-mismatch gaps */ 4708 .pd-side-card{flex:1 1 calc(50% - .5rem)} 4709 .pd-buy-card{flex:0 0 100%} 4710} 4711@media (max-width:880px){ 4712 .pd-top-layout{grid-template-columns:1fr} 4713 .pd-sidebar{flex-direction:column; padding: 0 1rem;} 4714 .pd-title{font-size:1.2rem} 4715 .pd-price-current{font-size:1.5rem} 4716} 4717@media (max-width:640px){ 4718 /* Related: 2 kolom di mobile */ 4719 .pd-related-full{padding:1.2rem;border:0;padding-top: .6rem;margin-bottom:.6rem;} 4720 .pd-related-grid,.pd-related-grid--full{grid-template-columns:repeat(2,1fr) !important;gap:12px !important} 4721} 4722@media (max-width:560px){ 4723 .pd-actions{flex-direction:column} 4724 .btn-add-cart,.btn-buy,.btn-buy-voucher{width:100%} 4725 .pd-full-description{padding:1.2rem;border:0;padding-bottom:0;} 4726} 4727@media (max-width:380px){ 4728 .pd-related-full{padding:.9rem .7rem 1.1rem} 4729 .pd-related-grid,.pd-related-grid--full{gap:8px !important} 4730 .pd-related-card .info{padding:.4rem .48rem .55rem} 4731} 4732 4733/* === Modern Countdown (Product Detail) === */ 4734.pd-countdown-modern{ 4735 --cd-bg1:#EAB308; /* gold-500 */ 4736 --cd-bg2:#B45309; /* indigo-500 */ 4737 --cd-text:#0b1220; 4738 --cd-contrast:#ffffff; 4739 --cd-shadow: 0 8px 24px -10px rgba(0,0,0,.25); 4740 --cd-border: rgba(255,255,255,.25); 4741 --cd-box: rgba(255,255,255,.65); 4742 --cd-box-text:#0f172a; 4743 --cd-danger:#ef4444; 4744 4745 display:flex; gap:.75rem; align-items:center; 4746 background:linear-gradient(135deg,var(--cd-bg1),var(--cd-bg2)); 4747 border:1px solid var(--cd-border); 4748 color:var(--cd-contrast); 4749 border-radius:14px; 4750 padding:.6rem .8rem; 4751 box-shadow:var(--cd-shadow); 4752 position:relative; 4753 overflow:hidden; 4754 margin-bottom: 1rem; 4755} 4756 4757.pd-countdown-modern .pd-cd-icon{ 4758 width:28px;height:28px;flex:0 0 28px; 4759 display:grid;place-items:center; 4760 background:rgba(255,255,255,.22); 4761 border:1px solid var(--cd-border); 4762 border-radius:10px; 4763 box-shadow: inset 0 1px 0 rgba(255,255,255,.35); 4764} 4765.pd-countdown-modern .pd-cd-icon svg{width:18px;height:18px;fill:#fff;opacity:.95} 4766
4767.pd-countdown-modern .pd-cd-body{display:flex;flex-direction:column;gap:.35rem;min-width:0} 4768 4769.pd-countdown-modern .pd-cd-label{ 4770 font-size:.72rem;font-weight:700;letter-spacing:.4px;opacity:.95;line-height:1; 4771 text-shadow:0 1px 0 rgba(0,0,0,.08); 4772} 4773 4774.pd-countdown-modern .pd-cd-time{ 4775 display:flex; align-items:end; gap:.35rem; flex-wrap:wrap; 4776 line-height:1; 4777} 4778 4779.pd-countdown-modern .unit{ 4780 display:flex; flex-direction:column; align-items:center; gap:.18rem; 4781 min-width:58px; 4782} 4783.pd-countdown-modern .num{ 4784 display:inline-flex; align-items:center; justify-content:center; 4785 min-width:58px; height:40px; padding:0 .4rem; 4786 background:var(--cd-box); 4787 color:var(--cd-box-text); 4788 border:1px solid rgba(15,23,42,.08); 4789 border-radius:10px; 4790 font: 800 1.05rem ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 4791 letter-spacing:.5px; 4792 box-shadow: 0 4px 14px -6px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.55); 4793} 4794.pd-countdown-modern .unit-label{ 4795 font-size:.62rem; font-weight:700; opacity:.9; text-shadow:0 1px 0 rgba(255,255,255,.15); 4796} 4797 4798.pd-countdown-modern .colon{ 4799 font-weight:900; font-size:1.05rem; opacity:.95; transform: translateY(-2px); 4800 animation: cdBlink 1.2s infinite steps(2, jump-none); 4801} 4802@keyframes cdBlink { 0%{opacity:1} 50%{opacity:.25} 100%{opacity:1} } 4803 4804.pd-countdown-modern .pd-cd-progress{ 4805 display:none; 4806 position:relative; 4807 height:6px; border-radius:999px; overflow:hidden; 4808 background:rgba(255,255,255,.25); 4809 border:1px solid rgba(255,255,255,.28); 4810} 4811.pd-countdown-modern .pd-cd-progress .bar{ 4812 height:100%; width:0%; background:linear-gradient(90deg,#22c55e,#a3e635); 4813 box-shadow: 0 2px 8px rgba(0,0,0,.25) inset; 4814 transition:width .35s ease; 4815} 4816 4817/* State saat hampir habis */ 4818.pd-countdown-modern.danger{ 4819 --cd-bg1:#CA8A04; /* gold-600 */ 4820 --cd-bg2:#ef4444; /* red-500 */ 4821} 4822 4823/* Responsive */ 4824@media (max-width:640px){ 4825 .pd-countdown-modern{ 4826 padding:.55rem .65rem; border-radius:12px; gap:.6rem; 4827 } 4828 .pd-countdown-modern .unit{min-width:52px} 4829 .pd-countdown-modern .num{min-width:52px;height:36px;font-size:.98rem} 4830} 4831 4832/* === Modern Countdown polish (shine + compact) === */ 4833.pd-countdown-modern{ 4834 position:relative; 4835} 4836.pd-countdown-modern::after{ 4837 content:""; 4838 position:absolute; inset:-1px; 4839 background: linear-gradient(120deg, rgba(255,255,255,.12), rgba(255,255,255,0) 35%), 4840 radial-gradient(120% 120% at 0% 0%, rgba(255,255,255,.08), rgba(255,255,255,0) 60%); 4841 border-radius: inherit; 4842 pointer-events:none; 4843 mix-blend-mode: screen; 4844 animation: sheen 4.5s ease-in-out infinite; 4845} 4846@keyframes sheen { 4847 0% { opacity:.35; transform: translateX(-10%) } 4848 50% { opacity:.15; transform: translateX(4%) } 4849 100% { opacity:.35; transform: translateX(-10%) } 4850} 4851 4852/* Mode compact (aktifkan dengan data-mode="compact" di elemen) */ 4853.pd-countdown-modern[data-mode="compact"]{ 4854 gap:.55rem; padding:.5rem .6rem; border-radius:12px; 4855} 4856.pd-countdown-modern[data-mode="compact"] .unit{ 4857 min-width:46px; 4858} 4859.pd-countdown-modern[data-mode="compact"] .num{ 4860 min-width:46px; height:34px; font-size:.92rem; 4861} 4862.pd-countdown-modern[data-mode="compact"] .pd-cd-label{ 4863 font-size:.66rem; 4864} 4865.pd-countdown-modern[data-mode="compact"] .pd-cd-icon{ 4866 width:24px; height:24px; border-radius:8px; 4867} 4868.pd-countdown-modern[data-mode="compact"] .pd-cd-progress{ 4869 height:5px; 4870} 4871 4872/* ============================================================ 4873 Sidebar: shared refinements for medium + tablet (641px â 1250px) 4874 Buy card = form & paylater on the same row. 4875 Alt-fintech logos compact (no over-stretching). 4876 ============================================================ */ 4877@media (min-width:641px) and (max-width:1250px) { 4878 /* Buy card: put qty/buy buttons and paylater promo inline */ 4879 .pd-buy-card { 4880 display: flex !important; 4881 flex-direction: row; 4882 flex-wrap: wrap; 4883 align-items: center; 4884 gap: .6rem .75rem; 4885 } 4886 .pd-buy-card .pd-buy-form { flex: 1 1 0; min-width: 0; } 4887 .pd-buy-card .paylater-promo { 4888 flex: 0 0 auto;
4889 align-self: center; 4890 margin: 0; 4891 max-width: 44%; 4892 } 4893 4894 /* Alt-fintech banner: tighter logos */ 4895 .iab-logos-compact { 4896 justify-content: center; 4897 gap: 1.25rem; 4898 } 4899 .iab-logos-compact .logo-fin { 4900 width: auto; 4901 max-height: 28px; 4902 object-fit: contain; 4903 } 4904} 4905 4906/* ============================================================ 4907 Tablet layout (641px â 880px) 4908 Restores 2-column grid + horizontal sidebar for mid-size screens. 4909 Overrides the generic â¤880px mobile-collapse rules above. 4910 ============================================================ */ 4911@media (min-width:641px) and (max-width:880px) { 4912 /* 2-column grid: image-card | info-card */ 4913 .pd-top-layout { 4914 grid-template-columns: minmax(260px, 40%) 1fr; 4915 gap: 1rem; 4916 } 4917 4918 /* Sidebar: full-width horizontal flex row below the two main columns */ 4919 .pd-sidebar { 4920 grid-column: 1 / -1; 4921 flex-direction: row; 4922 flex-wrap: wrap; 4923 gap: .75rem; 4924 padding: 0; 4925 } 4926 /* Banner cards fill 2-per-row */ 4927 .pd-side-card { flex: 1 1 calc(50% - .375rem); } 4928 /* Buy card: full row (tallest card â avoids white-space gap if shared with shorter banners) */ 4929 .pd-buy-card { display: block !important; flex: 0 0 100%; } 4930 4931 /* Hide installment simulator + alt-fintech banner on tablet (too much vertical space) */ 4932 .pd-installment-card2, 4933 .inst-alt-banner--compact { display: none !important; } 4934 4935 /* Restore sticky add-to-cart header (hidden at â¤880px by default) */ 4936 .pd-sticky-header { display: flex !important; } 4937 4938 /* Typography â tighten slightly to fit the narrower info column */ 4939 .pd-price-current { font-size: 1.65rem; } 4940 .pd-title { font-size: 1.1rem; } 4941 .pd-info-card { padding: 1.1rem 1.2rem 1.5rem; } 4942} 4943</style> 4944 <style data-inlined="product-grid.css"> 4945/* Product Grid (Firebird) */ 4946 4947.product-archive { 4948 max-width: 1320px; 4949 margin-inline: auto; 4950 padding: 0 0 3rem; 4951 /*font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;*/ 4952} 4953 4954.pa-toolbar { 4955 display: flex; 4956 flex-wrap: wrap; 4957 gap: 1rem; 4958 justify-content: space-between; 4959 align-items: flex-start; 4960} 4961 4962.pa-title { 4963 font-size: 1.4rem; 4964 margin: 0; 4965 font-weight: 600; 4966 line-height: 1.2; 4967 color: #0f172a; 4968} 4969 4970.pa-left { 4971 display: flex; 4972 flex-wrap: wrap; 4973 gap: .75rem; 4974 align-items: center; 4975} 4976 4977.pa-filter-badge { 4978 background: #f1f5f9; 4979 border: 1px solid #e2e8f0; 4980 padding: .45rem .6rem; 4981 border-radius: 8px; 4982 font-size: .7rem; 4983 display: flex; 4984 align-items: center; 4985 gap: .4rem; 4986 line-height: 1; 4987 color: #475569; 4988} 4989.pa-filter-badge strong { color: #0f172a; } 4990.pa-filter-badge .clear { 4991 display:inline-block; 4992 text-decoration:none; 4993 background:#e2e8f0; 4994 color:#475569; 4995 font-weight:600; 4996 padding:0 .35rem .05rem; 4997 border-radius:4px; 4998 transition:.2s; 4999 font-size:.75rem; 5000 line-height:1; 5001} 5002.pa-filter-badge .clear:hover { background:#cbd5e1; color:#1e293b; } 5003 5004.pa-right { 5005 display:flex; 5006 gap:1rem; 5007 flex-wrap:wrap; 5008 align-items:flex-start; 5009 margin-bottom: 0.5rem; 5010} 5011 5012.pa-search { 5013 display:flex; 5014 gap:.5rem; 5015 align-items:center; 5016 background:#fff; 5017 border:1px solid #e2e8f0; 5018 padding:.4rem .5rem; 5019 border-radius:8px; 5020} 5021.pa-search input[type=text] { 5022 border:0; 5023 outline:none; 5024 font-size:.8rem; 5025 padding:.35rem .4rem;
5026 min-width:200px; 5027} 5028.pa-search button { 5029 background:#0d6efd; 5030 border:0; 5031 color:#fff; 5032 font-size:.7rem; 5033 padding:.45rem .75rem; 5034 border-radius:6px; 5035 cursor:pointer; 5036 font-weight:600; 5037 letter-spacing:.3px; 5038 transition:.25s; 5039} 5040.pa-search button:hover { background:#0b5dc4; } 5041 5042.pa-sort { 5043 display:flex; 5044 gap:.4rem; 5045 align-items:center; 5046 font-size:.7rem; 5047 background:#fff; 5048 border:1px solid #e2e8f0; 5049 padding:.4rem .55rem; 5050 border-radius:8px; 5051} 5052.pa-sort select { 5053 font-size:.75rem; 5054 padding:.35rem .5rem; 5055 border:1px solid #cbd5e1; 5056 border-radius:6px; 5057 background:#fff; 5058 outline:none; 5059} 5060 5061.pa-empty { 5062 border:1px dashed #cbd5e1; 5063 padding:3rem 1rem; 5064 text-align:center; 5065 border-radius:12px; 5066 background:#fff; 5067} 5068.pa-empty p { 5069 margin:0 0 1rem; 5070 font-size:.9rem; 5071 color:#475569; 5072} 5073.btn-reset { 5074 display:inline-block; 5075 background:#0d6efd; 5076 color:#fff; 5077 font-size:.7rem; 5078 padding:.55rem 1rem; 5079 border-radius:8px; 5080 text-decoration:none; 5081 font-weight:600; 5082 letter-spacing:.4px; 5083 transition:.25s; 5084} 5085.btn-reset:hover { background:#0a58c7; } 5086 5087/* Grid */ 5088.pa-grid { 5089 display:grid; 5090 gap:1rem; 5091 grid-template-columns:repeat(auto-fill, minmax(180px,1fr)); 5092 align-items:stretch; 5093} 5094 5095.pa-card { 5096 background:#fff; 5097 border:1px solid #e2e8f0; 5098 border-radius:12px; 5099 display:flex; 5100 flex-direction:column; 5101 text-decoration:none; 5102 color:inherit; 5103 position:relative; 5104 overflow:hidden; 5105 transition:.28s cubic-bezier(.4,0,.2,1); 5106 min-width:0; 5107 padding: 0; 5108} 5109.pa-card:hover { 5110 transform:translateY(-4px); 5111 box-shadow:0 8px 22px -8px rgba(0,0,0,.15); 5112 border-color:#dce2ea; 5113} 5114 5115.pa-card .thumb { 5116 aspect-ratio:1/1; 5117 background:#f1f5f9; 5118 display:flex; 5119 align-items:center; 5120 justify-content:center; 5121 overflow:hidden; 5122 position:relative; 5123} 5124.pa-card img { 5125 width:100%; 5126 height:100%; 5127 object-fit:cover; 5128 display:block; 5129} 5130.badge-disc { 5131 position:absolute; 5132 top:8px; 5133 left:8px; 5134 background:#dc2626; 5135 color:#fff; 5136 font-size:.6rem; 5137 padding:4px 6px 3px; 5138 border-radius:6px; 5139 font-weight:700; 5140 letter-spacing:.4px; 5141} 5142 5143.pa-card .body { 5144 padding:.65rem .7rem .8rem; 5145 display:flex; 5146 flex-direction:column; 5147 gap:.45rem; 5148 min-height:90px; 5149} 5150 5151.pa-card .title { 5152 font-size:.75rem; 5153 line-height:1.25; 5154 margin:0; 5155 font-weight:600;
5156 color:#1e293b; 5157 height:2.4em; 5158 overflow:hidden; 5159} 5160 5161.pa-card .prices { 5162 display:flex; 5163 flex-wrap:wrap; 5164 align-items:baseline; 5165 gap:.4rem; 5166} 5167.pa-card .prices .final { 5168 font-size:.83rem; 5169 font-weight:700; 5170 color:#0d6efd; 5171} 5172.pa-card .prices .ori { 5173 font-size:.63rem; 5174 text-decoration:line-through; 5175 color:#94a3b8; 5176 font-weight:500; 5177} 5178 5179.pa-card .cat { 5180 margin-top:auto; 5181 font-size:.55rem; 5182 letter-spacing:.6px; 5183 text-transform:uppercase; 5184 background:#f1f5f9; 5185 border:1px solid #e2e8f0; 5186 padding:.28rem .45rem .25rem; 5187 border-radius:20px; 5188 font-weight:600; 5189 color:#475569; 5190 align-self:flex-start; 5191} 5192.pa-card .meta-line { 5193 margin-top:4px; 5194 font-size:.65rem; 5195 color:#475569; 5196} 5197.pa-card .meta-line .sold { 5198 display:inline-block; 5199 background:#f1f5f9; 5200 padding:2px 6px 3px; 5201 border-radius:4px; 5202 font-weight:600; 5203 letter-spacing:.3px; 5204 color:#334155; 5205} 5206 5207/* Pagination */ 5208.pa-pagination { 5209 margin-top:2rem; 5210 display:flex; 5211 flex-wrap:wrap; 5212 gap:.4rem; 5213 justify-content:center; 5214 font-size:.7rem; 5215 align-items:center; 5216} 5217.pa-pagination a, 5218.pa-pagination span { 5219 min-width:34px; 5220 height:34px; 5221 display:flex; 5222 align-items:center; 5223 justify-content:center; 5224 border-radius:8px; 5225 text-decoration:none; 5226 font-weight:600; 5227 padding:0 .6rem; 5228 box-sizing:border-box; 5229 border:1px solid #e2e8f0; 5230 background:#fff; 5231 color:#334155; 5232 transition:.25s; 5233} 5234.pa-pagination a:hover { 5235 background:#f1f5f9; 5236 color:#0f172a; 5237} 5238.pa-pagination .current { 5239 background:#0d6efd; 5240 color:#fff; 5241 border-color:#0d6efd; 5242} 5243.pa-pagination .gap { 5244 border:none; 5245 background:transparent; 5246 font-weight:400; 5247 color:#94a3b8; 5248} 5249 5250/* =============== Mobile Filter Bar (Tokopedia-like) =============== */ 5251.pa-mobile-filterbar{display:none} 5252@media (max-width: 640px){ 5253 /* sembunyikan tombol lama */ 5254 .pa-btn-filter{display:none} 5255 5256 .pa-mobile-filterbar{ 5257 display:block; 5258 position:sticky; 5259 top:0; /* sesuaikan jika ada header fixed di atas */ 5260 z-index:5; 5261 background:#fff; 5262 border-bottom:1px solid #e2e8f0; 5263 padding:.4rem .5rem; 5264 margin:-.4rem -1rem .6rem; /* full-bleed di container mobile */ 5265 } 5266 .pa-mobile-filterbar .mf-scroll{ 5267 display:flex;align-items:center;gap:.5rem; 5268 overflow-x:auto; 5269 -webkit-overflow-scrolling:touch; 5270 scrollbar-width:none; 5271 } 5272 .pa-mobile-filterbar .mf-scroll::-webkit-scrollbar{display:none} 5273 5274 .mf-chip{ 5275 flex:0 0 auto; 5276 background:#fff; 5277 color:#334155; 5278 text-decoration:none; 5279 border:1px solid #e2e8f0; 5280 border-radius:999px; 5281 padding:.44rem .7rem; 5282 font-size:.78rem; 5283 font-weight:600; 5284 letter-spacing:.2px; 5285 white-space:nowrap; 5286 } 5287 .mf-chip:active{transform:scale(.98)} 5288 .mf-chip.active{ 5289 background:#eaf2ff; 5290 border-color:#0d6efd; 5291 color:#0d6efd; 5292 } 5293 5294 .mf-filter-btn{ 5295 position:relative; 5296 flex:0 0 auto; 5297 width:38px;height:38px; 5298 border-radius:50%; 5299 border:1.5px solid #0d6efd; 5300 background:#fff; 5301 display:inline-flex;align-items:center;justify-content:center; 5302 color:#0d6efd; 5303 cursor:pointer; 5304 } 5305 .mf-filter-btn:active{transform:scale(.96)} 5306 .mf-filter-btn svg{width:18px;height:18px;display:block} 5307 .mf-filter-btn .mf-badge{ 5308 position:absolute;top:0;right:-4px; 5309 min-width:18px;height:18px;border-radius:9px; 5310 background:#ef4444;color:#fff; 5311 font-size:.65rem;font-weight:700; 5312 display:flex;align-items:center;justify-content:center; 5313 padding:0 4px;line-height:1; 5314 box-shadow:0 2px 6px -2px rgba(239,68,68,.6); 5315 } 5316} 5317 5318/* Responsive */ 5319@media (max-width: 640px) { 5320 .product-archive { padding: 0 1rem 3rem; } 5321 .pa-grid { grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); } 5322 .pa-search input[type=text]{ min-width:140px; } 5323 .pa-toolbar { flex-direction:column; align-items:stretch; margin-bottom: 0; display: none;} 5324 .pa-right { display: none; } 5325 .pa-sort { width: 100%; justify-content:center; } 5326 .pa-mobile-filterbar { display: flex !important; } 5327} 5328 5329.pa-mobile-filterbar { 5330 display: none; 5331 align-items: center; 5332 gap: 8px; 5333 padding: 8px 0 8px 12px; 5334 background: #fff; 5335 width: 100vw; 5336} 5337.pa-mobile-filterbar .mf-scroll { 5338 display: flex; 5339 gap: 8px; 5340 overflow-x: auto; 5341 -webkit-overflow-scrolling: touch; 5342 scrollbar-width: none; /* Firefox */ 5343 flex: 1 1 auto; /* takes remaining width */ 5344} 5345.pa-mobile-filterbar .mf-scroll::-webkit-scrollbar { display: none; } /* WebKit */ 5346.pa-mobile-filterbar .mf-chip { 5347 flex: 0 0 auto; /* prevent shrinking, keep width based on content */ 5348 white-space: nowrap; 5349} 5350.pa-mobile-filterbar .mf-filter-btn { 5351 flex: 0 0 auto; /* do not shrink */ 5352 position: relative; /* keep in place, not inside scroller */ 5353 display: inline-flex; 5354 align-items: center; 5355 gap: 6px; 5356} 5357 5358/* Optional: ensure on small screens the button stays visible with min width */ 5359@media (max-width: 768px) { 5360 .pa-mobile-filterbar .mf-filter-btn { 5361 padding: 6px 10px; 5362 } 5363} 5364 5365/* Product Specification Chips */ 5366.pa-card .spec-chips { 5367 font-size: .6rem; 5368 color: #64748b; 5369 font-weight: 500; 5370 letter-spacing: .2px; 5371 margin-top: 2px; 5372 margin-bottom: 4px;
5373 line-height: 1.2; 5374 opacity: 0.9; 5375 max-width: 100%; 5376 overflow: hidden; 5377 text-overflow: ellipsis; 5378 white-space: nowrap; 5379} 5380 5381.pa-card .spec-chips .spec-divider { 5382 color: #94a3b8; 5383 font-weight: 400; 5384 margin: 0 1px; 5385} 5386</style> 5387 <style data-inlined="site-notice.css"> 5388/** 5389 * Site Notice Banner - Reusable Notification System 5390 * Dismissible top bar for announcements, holidays, maintenance, etc. 5391 */ 5392 5393.holiday-notice { 5394 position: fixed; 5395 top: 0; 5396 left: 0; 5397 right: 0; 5398 z-index: 9999; 5399 background: linear-gradient(135deg, #d32f2f 0%, #8B6914 100%); 5400 border-bottom: 2px solid rgba(255, 255, 255, 0.2); 5401 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 5402 animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1); 5403 transform-origin: top; 5404} 5405 5406.holiday-notice.hidden { 5407 animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 5408} 5409 5410@keyframes slideDown { 5411 from { 5412 transform: translateY(-100%); 5413 opacity: 0; 5414 } 5415 to { 5416 transform: translateY(0); 5417 opacity: 1; 5418 } 5419} 5420 5421@keyframes slideUp { 5422 from { 5423 transform: translateY(0); 5424 opacity: 1; 5425 } 5426 to { 5427 transform: translateY(-100%); 5428 opacity: 0; 5429 } 5430} 5431 5432.holiday-notice-container { 5433 max-width: 1400px; 5434 margin: 0 auto; 5435 padding: 12px 20px; 5436 display: flex;
5437 align-items: center; 5438 justify-content: space-between; 5439 gap: 16px; 5440} 5441 5442.holiday-notice-content { 5443 display: flex; 5444 align-items: center; 5445 gap: 12px; 5446 flex: 1; 5447} 5448 5449.holiday-notice-icon { 5450 font-size: 20px; 5451 line-height: 1; 5452 flex-shrink: 0; 5453} 5454 5455.holiday-notice-text { 5456 color: #ffffff; 5457 font-size: 14px; 5458 font-weight: 500; 5459 line-height: 1.5; 5460 margin: 0; 5461 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 5462} 5463 5464.holiday-notice-emoji { 5465 display: inline-block; 5466 animation: bounce 2s ease-in-out infinite; 5467} 5468 5469@keyframes bounce { 5470 0%, 100% { 5471 transform: translateY(0); 5472 } 5473 50% { 5474 transform: translateY(-4px); 5475 } 5476} 5477 5478.holiday-notice-close { 5479 background: rgba(255, 255, 255, 0.2); 5480 border: 1px solid rgba(255, 255, 255, 0.3); 5481 border-radius: 6px; 5482 padding: 6px; 5483 cursor: pointer; 5484 color: #ffffff; 5485 transition: all 0.2s ease; 5486 flex-shrink: 0; 5487 display: flex; 5488 align-items: center; 5489 justify-content: center; 5490 width: 32px; 5491 height: 32px; 5492} 5493 5494.holiday-notice-close:hover { 5495 background: rgba(255, 255, 255, 0.3); 5496 transform: scale(1.05); 5497} 5498 5499.holiday-notice-close:active { 5500 transform: scale(0.95); 5501} 5502 5503.holiday-notice-close:focus { 5504 outline: 2px solid rgba(255, 255, 255, 0.5); 5505 outline-offset: 2px; 5506} 5507 5508/* Body padding adjustment when banner is visible */ 5509body.has-holiday-notice { 5510 padding-top: 52px; 5511} 5512 5513/* Tablet Styles */ 5514@media (max-width: 1024px) { 5515 .holiday-notice-container { 5516 padding: 10px 16px; 5517 } 5518 5519 .holiday-notice-text { 5520 font-size: 13px; 5521 } 5522 5523 .holiday-notice-icon { 5524 font-size: 18px; 5525 } 5526 5527 body.has-holiday-notice { 5528 padding-top: 48px; 5529 } 5530} 5531 5532/* Mobile Styles */ 5533@media (max-width: 640px) { 5534 .holiday-notice-container { 5535 padding: 10px 12px; 5536 gap: 8px; 5537 } 5538 5539 .holiday-notice-content { 5540 gap: 8px; 5541 } 5542 5543 .holiday-notice-text { 5544 font-size: 12px; 5545 line-height: 1.4; 5546 } 5547 5548 .holiday-notice-icon { 5549 font-size: 16px; 5550 } 5551 5552 .holiday-notice-close { 5553 width: 28px; 5554 height: 28px; 5555 } 5556 5557 .holiday-notice-close svg { 5558 width: 16px; 5559 height: 16px; 5560 } 5561 5562 body.has-holiday-notice { 5563 padding-top: 44px; 5564 } 5565} 5566 5567/* Ensure header doesn't get covered - removed z-index as it conflicts with dropdown stacking */ 5568/* .header, 5569.navbar, 5570header { 5571 position: relative; 5572 z-index: 100; 5573} */ 5574 5575</style> 5576 <style data-inlined="benchmark.css"> 5577/** 5578 * Benchmark Section Styles 5579 * For product detail page benchmark display 5580 */ 5581 5582.pd-benchmark-section { 5583 background: #fff; 5584 border-radius: 12px; 5585 padding: 24px; 5586 margin: 32px 0; 5587 box-shadow: 0 2px 8px rgba(0,0,0,0.06); 5588 border: 1px solid #e5e7eb; 5589} 5590 5591.benchmark-header { 5592 margin-bottom: 20px; 5593} 5594 5595.benchmark-title { 5596 font-size: 22px; 5597 font-weight: 700; 5598 color: #1a1a1a; 5599 margin: 0 0 12px 0; 5600 display: flex;
5601 align-items: center; 5602 gap: 8px; 5603} 5604 5605.benchmark-hint { 5606 font-size: 14px; 5607 color: #6b7280; 5608 margin: 0; 5609} 5610 5611/* Preview Badges */ 5612.benchmark-preview-badges { 5613 display: flex; 5614 flex-wrap: wrap; 5615 gap: 10px; 5616 margin-top: 12px; 5617} 5618 5619.benchmark-badge { 5620 display: inline-flex; 5621 align-items: center; 5622 gap: 6px; 5623 background: linear-gradient(135deg, #EAB308 0%, #8B6914 100%); 5624 color: #fff; 5625 padding: 8px 16px; 5626 border-radius: 20px; 5627 font-size: 13px; 5628 font-weight: 500; 5629 box-shadow: 0 2px 6px rgba(102, 126, 234, 0.25); 5630 transition: all 0.2s ease; 5631} 5632 5633.benchmark-badge:hover { 5634 transform: translateY(-2px); 5635 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35); 5636} 5637 5638.benchmark-badge strong { 5639 font-weight: 600; 5640 opacity: 0.9; 5641} 5642 5643/* Load Button */ 5644.btn-load-benchmark { 5645 display: flex; 5646 align-items: center; 5647 justify-content: center; 5648 gap: 10px; 5649 width: 100%; 5650 max-width: 420px; 5651 padding: 14px 24px; 5652 background: linear-gradient(135deg, #EAB308 0%, #8B6914 100%); 5653 color: #fff; 5654 border: none; 5655 border-radius: 8px; 5656 font-size: 15px; 5657 font-weight: 600; 5658 cursor: pointer; 5659 transition: all 0.3s ease; 5660 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 5661 margin: 0 auto; 5662} 5663 5664.btn-load-benchmark:hover:not(:disabled) { 5665 transform: translateY(-2px); 5666 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); 5667} 5668 5669.btn-load-benchmark:disabled { 5670 opacity: 0.6; 5671 cursor: not-allowed; 5672} 5673 5674.btn-load-benchmark .btn-icon { 5675 font-size: 20px; 5676} 5677 5678.btn-load-benchmark.loading .btn-icon { 5679 animation: spin 1s linear infinite; 5680} 5681 5682@keyframes spin { 5683 from { transform: rotate(0deg); } 5684 to { transform: rotate(360deg); } 5685} 5686 5687/* Benchmark Content */ 5688.benchmark-content { 5689 margin-top: 24px; 5690 padding-top: 24px; 5691 border-top: 2px solid #f3f4f6; 5692} 5693 5694.benchmark-loading { 5695 text-align: center; 5696 padding: 40px 20px; 5697 color: #6b7280; 5698} 5699 5700.benchmark-loading-spinner { 5701 width: 48px; 5702 height: 48px; 5703 margin: 0 auto 16px; 5704 border: 4px solid #f3f4f6; 5705 border-top-color: #EAB308; 5706 border-radius: 50%; 5707 animation: spin 1s linear infinite; 5708} 5709 5710.benchmark-error { 5711 text-align: center; 5712 padding: 32px 20px; 5713 color: #ef4444; 5714 background: #fee2e2; 5715 border-radius: 8px; 5716} 5717 5718/* Benchmark Data Display */ 5719.benchmark-data-grid { 5720 display: grid; 5721 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 5722 gap: 16px; 5723 margin-bottom: 24px; 5724} 5725 5726.benchmark-card { 5727 background: linear-gradient(135deg, #EAB30815 0%, #8B691415 100%); 5728 border: 1px solid #e5e7eb; 5729 border-radius: 10px; 5730 padding: 18px; 5731 text-align: center; 5732 transition: all 0.2s ease; 5733} 5734 5735.benchmark-card:hover { 5736 transform: translateY(-3px); 5737 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.15); 5738} 5739 5740.benchmark-card-label { 5741 font-size: 13px; 5742 color: #6b7280; 5743 font-weight: 500; 5744 margin-bottom: 8px; 5745 text-transform: uppercase; 5746 letter-spacing: 0.5px; 5747} 5748 5749.benchmark-card-value { 5750 font-size: 24px; 5751 font-weight: 700; 5752 color: #1a1a1a; 5753 line-height: 1.2; 5754} 5755 5756.benchmark-card-value.large { 5757 font-size: 28px; 5758} 5759 5760.benchmark-card-subtitle { 5761 font-size: 12px; 5762 color: #9ca3af; 5763 margin-top: 4px; 5764} 5765 5766/* Spec Details */ 5767.benchmark-specs { 5768 background: #f9fafb; 5769 border-radius: 10px; 5770 padding: 20px; 5771 margin-top: 20px; 5772} 5773 5774.benchmark-specs-title { 5775 font-size: 16px; 5776 font-weight: 600; 5777 color: #1a1a1a; 5778 margin: 0 0 16px 0; 5779} 5780 5781.benchmark-spec-row { 5782 display: flex; 5783 justify-content: space-between; 5784 padding: 10px 0; 5785 border-bottom: 1px solid #e5e7eb; 5786} 5787 5788.benchmark-spec-row:last-child { 5789 border-bottom: none; 5790} 5791 5792.benchmark-spec-label { 5793 font-size: 14px; 5794 color: #6b7280; 5795 font-weight: 500; 5796} 5797 5798.benchmark-spec-value { 5799 font-size: 14px; 5800 color: #1a1a1a; 5801 font-weight: 600; 5802 text-align: right; 5803} 5804 5805/* Score Badges */ 5806.benchmark-score-badge { 5807 display: inline-block; 5808 padding: 6px 14px; 5809 border-radius: 20px; 5810 font-size: 13px; 5811 font-weight: 600; 5812 margin-top: 8px; 5813} 5814 5815.benchmark-score-badge.excellent { 5816 background: #10b981; 5817 color: #fff; 5818} 5819 5820.benchmark-score-badge.good { 5821 background: #EAB308; 5822 color: #fff; 5823} 5824 5825.benchmark-score-badge.average { 5826 background: #CA8A04; 5827 color: #fff; 5828} 5829 5830.benchmark-score-badge.poor { 5831 background: #ef4444; 5832 color: #fff; 5833} 5834 5835/* Attribution */ 5836.benchmark-attribution { 5837 text-align: center; 5838 margin-top: 24px; 5839 padding-top: 20px; 5840 border-top: 1px solid #e5e7eb; 5841 font-size: 13px; 5842 color: #9ca3af; 5843} 5844 5845.benchmark-attribution a { 5846 color: #EAB308; 5847 text-decoration: none; 5848 font-weight: 500; 5849} 5850 5851.benchmark-attribution a:hover { 5852 text-decoration: underline; 5853} 5854 5855/* Responsive */ 5856@media (max-width: 768px) { 5857 .pd-benchmark-section { 5858 padding: 16px; 5859 margin: 20px 0; 5860 } 5861 5862 .benchmark-title { 5863 font-size: 18px; 5864 } 5865 5866 .benchmark-preview-badges { 5867 flex-direction: column; 5868 gap: 8px; 5869 } 5870 5871 .benchmark-badge { 5872 justify-content: center; 5873 } 5874 5875 .btn-load-benchmark { 5876 font-size: 14px; 5877 padding: 12px 20px; 5878 } 5879 5880 .benchmark-data-grid { 5881 grid-template-columns: 1fr 1fr; 5882 gap: 12px; 5883 } 5884 5885 .benchmark-card { 5886 padding: 14px; 5887 } 5888 5889 .benchmark-card-value { 5890 font-size: 20px; 5891 } 5892 5893 .benchmark-spec-row { 5894 flex-direction: column; 5895 gap: 4px; 5896 } 5897 5898 .benchmark-spec-value { 5899 text-align: left; 5900 } 5901} 5902 5903@media (max-width: 480px) { 5904 .benchmark-data-grid { 5905 grid-template-columns: 1fr; 5906 } 5907} 5908 5909</style> 5910 <style data-inlined="product-filters.css"> 5911/* ========================================================= 5912 Product Filters (Desktop + Mobile Drawer) 5913 Light Only + FIX: 5914 - Desktop: f-actions selalu tampil (static bottom) 5915 - Mobile: konten filter dapat discroll 5916 ========================================================= */ 5917 5918/* ========== Layout wrapper (tidak mengubah grid produk) ========== */ 5919.product-archive.with-filters .pa-layout { 5920 display:flex; 5921 gap:24px; 5922 align-items:flex-start; 5923} 5924.pa-main { flex:1; min-width:0; } 5925 5926/* ========================================================= 5927 DESKTOP PANEL 5928 ========================================================= */ 5929.pa-filters { 5930 width:300px; 5931 background:linear-gradient(145deg,#ffffff 0%,#f8fafc 55%,#f1f5f9 100%); 5932 border:1px solid #e2e8f0; 5933 padding:18px 18px 12px; /* bottom dikurangi karena f-actions punya padding sendiri */ 5934 border-radius:18px; 5935 position:sticky; 5936 top:80px; 5937 height:calc(100vh - 100px); /* tinggi stabil untuk inner scroll */ 5938 font-size:14px; 5939 box-shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(15,23,42,.08); 5940 backdrop-filter:blur(3px); 5941 -webkit-backdrop-filter:blur(3px);
5942 transition:box-shadow .3s, background .4s; 5943 display:flex; 5944 flex-direction:column; 5945 overflow:hidden; /* cegah double scroll, inner yang scroll */ 5946} 5947.pa-filters::before { 5948 content:""; 5949 position:absolute; 5950 inset:0 auto 0 0; 5951 width:7px; 5952 border-radius:18px 0 0 18px; 5953 background:linear-gradient(180deg,#CA8A04 0%,#FBBF24 35%,#C9A227 100%); 5954} 5955.pa-filters.active { box-shadow:0 8px 32px -4px rgba(15,23,42,.35); } 5956 5957/* Scroll area (desktop/mode shared) */ 5958.filters-scroll { 5959 flex:1; 5960 min-height:0; /* wajib agar overflow terbaca dalam flex container */ 5961 overflow:auto; 5962 padding-right:6px; 5963 margin-right:-6px; /* kompensasi scrollbar */ 5964 scrollbar-width:thin; 5965 overscroll-behavior:contain; 5966} 5967.filters-scroll::-webkit-scrollbar { width:8px; } 5968.filters-scroll::-webkit-scrollbar-thumb { 5969 background:#cbd5e1; 5970 border-radius:20px; 5971 border:2px solid #f1f5f9; 5972} 5973.filters-scroll::-webkit-scrollbar-thumb:hover { background:#94a3b8; } 5974 5975/* Heading */ 5976.pa-filters h2 { 5977 margin:0 0 14px; 5978 font-size:20px; 5979 font-weight:700; 5980 letter-spacing:.4px; 5981 line-height:1.15; 5982 background:linear-gradient(90deg,#0f172a,#C9A227 60%,#0f172a); 5983 -webkit-background-clip:text; 5984 color:transparent; 5985 background-size:200% 100%; 5986 animation:titleSheen 8s linear infinite; 5987} 5988@keyframes titleSheen { 5989 0%{background-position:0 0} 5990 50%{background-position:100% 0} 5991 100%{background-position:0 0} 5992} 5993 5994/* Form groups */ 5995.pa-filters .f-group { margin-bottom:18px; position:relative; } 5996.pa-filters label { 5997 display:block; 5998 font-weight:600; 5999 margin:0 0 6px; 6000 font-size:13px; 6001 color:#0f172a; 6002 letter-spacing:.3px; 6003} 6004 6005/* Inputs / selects */ 6006.pa-filters input[type=text], 6007.pa-filters input[type=number], 6008.pa-filters select { 6009 width:100%; 6010 padding:7px 10px; 6011 border:1px solid #cbd5e1; 6012 border-radius:8px; 6013 font-size:13px; 6014 min-height:34px; 6015 background:#ffffff; 6016 box-sizing:border-box; 6017 color:#0f172a; 6018 transition:border-color .25s, box-shadow .25s, background .25s; 6019 -webkit-appearance:none; 6020 -moz-appearance:none; 6021 appearance:none; 6022} 6023.pa-filters select { 6024 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"); 6025 background-repeat:no-repeat; 6026 background-position:right 10px center; 6027 background-size:12px; 6028 padding-right:32px; 6029} 6030.pa-filters input[type=text]:hover, 6031.pa-filters input[type=number]:hover, 6032.pa-filters select:hover { border-color:#94b5ec; } 6033.pa-filters input[type=text]:focus, 6034.pa-filters input[type=number]:focus, 6035.pa-filters select:focus { 6036 outline:none; 6037 border-color:#C9A227; 6038 box-shadow:0 0 0 3px rgba(201,162,39,.25); 6039 background:#f8fbff; 6040} 6041.pa-filters .range { display:flex; gap:10px; } 6042.pa-filters .range input { flex:1; } 6043.pa-filters .hint { font-size:11px; color:#64748b; margin-top:6px; } 6044 6045/* Spec blocks */ 6046.f-specs { margin-top:4px; } 6047.spec-attr { 6048 margin:0 0 14px; 6049 padding:12px 12px 10px; 6050 border:1px solid #e2e8f0; 6051 border-radius:14px; 6052 background:linear-gradient(130deg,#ffffff 0%,#f7faff 85%); 6053 position:relative; 6054 transition:border-color .3s, box-shadow .3s, background .4s; 6055} 6056.spec-attr.collapsed { padding-bottom:8px; } 6057.spec-attr.has-selected { 6058 border-color:#C9A227; 6059 box-shadow:0 0 0 2px rgba(201,162,39,.15),0 2px 8px -2px rgba(201,162,39,.35); 6060 background:linear-gradient(135deg,#ffffff 0%,#e9f2ff 100%); 6061} 6062.spec-attr-title { 6063 font-weight:600; 6064 margin:0 0 8px; 6065 font-size:13px; 6066 display:flex; 6067 justify-content:space-between; 6068 align-items:center; 6069 cursor:pointer; 6070 color:#0f172a; 6071 user-select:none; 6072} 6073.spec-attr-title .chevron { 6074 font-size:10px; 6075 transition:transform .25s, opacity .25s; 6076 opacity:.65; 6077} 6078.spec-attr.collapsed .spec-attr-title .chevron { 6079 transform:rotate(-90deg); 6080 opacity:.5; 6081} 6082 6083/* Spec options */ 6084.spec-attr-options { display:flex; flex-wrap:wrap; gap:6px; } 6085.spec-attr-options label { 6086 display:inline-flex; 6087 align-items:center; 6088 gap:4px; 6089 font-weight:500; 6090 border:1px solid #d4dde7; 6091 padding:6px 9px; 6092 border-radius:30px; 6093 cursor:pointer; 6094 background:#f3f6fa; 6095 font-size:12px; 6096 line-height:1.1; 6097 transition:background .25s, border-color .25s, color .25s, box-shadow .25s; 6098 position:relative; 6099} 6100.spec-attr-options label:hover { background:#e8f1ff; border-color:#b1cff3; } 6101.spec-attr-options input { 6102 margin:0; 6103 position:absolute; 6104 inset:0; 6105 opacity:0;
6106 cursor:pointer; 6107} 6108.spec-attr-options label:has(input:checked) { 6109 background:linear-gradient(135deg,#C9A227 0%,#A16207 100%); 6110 border-color:#C9A227; 6111 color:#ffffff; 6112 box-shadow:0 2px 6px -1px rgba(201,162,39,.55); 6113} 6114.spec-attr-options label:has(input:checked)::after { 6115 content:""; 6116 position:absolute; 6117 right:6px; 6118 top:6px; 6119 width:6px; 6120 height:6px; 6121 background:#fff; 6122 border-radius:50%; 6123 box-shadow:0 0 0 4px rgba(255,255,255,.25),0 0 0 7px rgba(201,162,39,.5); 6124 animation:pulseDot 1.9s ease-out infinite; 6125} 6126@keyframes pulseDot { 6127 0%{transform:scale(.8);opacity:.9} 6128 60%{transform:scale(1.4);opacity:.15} 6129 100%{transform:scale(.8);opacity:.9} 6130} 6131 6132/* Badges */ 6133.pa-filter-badges { margin:0 0 18px; display:flex; flex-wrap:wrap; gap:6px; } 6134.pa-filter-badge { 6135 display:inline-flex; 6136 gap:6px; 6137 align-items:center; 6138 background:linear-gradient(135deg,#eef4ff 0%,#e0edff 100%); 6139 border:1px solid #b4cff3; 6140 padding:5px 10px 5px 12px; 6141 border-radius:22px; 6142 margin:2px 4px 0 0; 6143 font-size:11px; 6144 font-weight:500; 6145 line-height:1; 6146 color:#0f172a; 6147 position:relative; 6148 overflow:hidden; 6149} 6150.pa-filter-badge::before { 6151 content:""; 6152 position:absolute; 6153 inset:0; 6154 background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.35),rgba(255,255,255,0)); 6155 transform:translateX(-100%); 6156 animation:badgeSheen 6s linear infinite; 6157} 6158@keyframes badgeSheen { 6159 0%{transform:translateX(-100%)} 6160 55%{transform:translateX(120%)} 6161 100%{transform:translateX(120%)} 6162} 6163.pa-filter-badge a.clear { 6164 text-decoration:none; 6165 color:#c02828; 6166 font-weight:700; 6167 padding:0 3px; 6168 display:inline-flex; 6169 align-items:center; 6170 justify-content:center; 6171 border-radius:50%; 6172 line-height:1; 6173 transition:background .25s, color .25s; 6174} 6175.pa-filter-badge a.clear:hover { background:#ffeded; color:#a40000; } 6176.pa-btn-filter { width: 100%;} 6177 6178/* ========================================================= 6179 ACTIONS (DESKTOP) - STATIC BOTTOM 6180 ========================================================= */ 6181.f-actions { 6182 margin-top:10px; 6183 display:flex; 6184 gap:12px; 6185 flex-wrap:wrap; 6186 padding:16px 0 4px; 6187 border-top:1px solid #e2e8f0; 6188 background:linear-gradient(180deg,rgba(255,255,255,0) 0%,#f1f5f9 70%); 6189 flex-shrink:0; 6190} 6191.f-actions button { 6192 background:linear-gradient(135deg,#C9A227 0%,#A16207 100%); 6193 color:#fff; 6194 border:none; 6195 padding:10px 20px; 6196 border-radius:12px; 6197 cursor:pointer; 6198 font-size:14px; 6199 font-weight:600; 6200 letter-spacing:.3px; 6201 display:inline-flex; 6202 align-items:center; 6203 gap:6px; 6204 box-shadow:0 4px 14px -2px rgba(201,162,39,.45); 6205 transition:transform .25s, box-shadow .3s; 6206} 6207.f-actions button:hover { transform:translateY(-3px); box-shadow:0 8px 26px -4px rgba(201,162,39,.6); } 6208.f-actions button:active { transform:translateY(-1px) scale(.98); box-shadow:0 4px 14px -4px rgba(201,162,39,.55); } 6209.f-actions .reset { 6210 font-size:12px; 6211 text-decoration:none; 6212 color:#d92d2d; 6213 align-self:center; 6214 font-weight:600; 6215 padding:4px 6px; 6216 border-radius:8px; 6217 transition:background .25s, color .25s; 6218} 6219.f-actions .reset:hover { background:#ffe6e6; color:#b80000; text-decoration:underline; } 6220.pa-filters .close-drawer { display: none; } 6221 6222/* Form wrapper: flex container agar inner .filters-scroll bisa scroll di desktop */ 6223.filter-form { 6224 flex: 1; 6225 min-height: 0; 6226 display: flex; 6227 flex-direction: column; 6228 overflow: hidden; 6229} 6230 6231/* ========================================================= 6232 FILTER TOGGLE (MOBILE TRIGGER) 6233 ========================================================= */ 6234.filter-toggle { 6235 display:none; 6236 margin:0 0 14px; 6237 background:linear-gradient(135deg,#C9A227 0%,#A16207 100%); 6238 color:#fff; 6239 border:none; 6240 padding:11px 18px; 6241 font-size:14px; 6242 font-weight:600; 6243 border-radius:14px; 6244 cursor:pointer; 6245 letter-spacing:.4px; 6246 box-shadow:0 4px 14px -2px rgba(201,162,39,.55); 6247 position:relative; 6248 z-index:100; 6249 transition:box-shadow .3s, transform .25s; 6250} 6251.filter-toggle:hover { box-shadow:0 8px 26px -4px rgba(201,162,39,.6); transform:translateY(-2px); } 6252.filter-toggle:active { transform:translateY(0); box-shadow:0 4px 14px -4px rgba(201,162,39,.55); } 6253.filter-toggle .count-badge { 6254 display:inline-block; 6255 background:linear-gradient(135deg,#CA8A04 0%,#FBBF24 55%,#CA8A04 100%); 6256 color:#1d2b44; 6257 font-size:11px; 6258 font-weight:700; 6259 padding:3px 7px 4px;
6260 border-radius:30px; 6261 margin-left:8px; 6262 box-shadow:0 0 0 2px #ffffff,0 0 0 4px #C9A227; 6263} 6264 6265/* Overlay (mobile) */ 6266.pa-filters-overlay { display:none; } 6267 6268/* ========================================================= 6269 MOBILE DRAWER 6270 ========================================================= */ 6271@media (max-width:768px){ 6272 .product-archive.with-filters .pa-layout { flex-direction:column; gap:12px; } 6273 .filter-toggle { display:inline-flex; align-items:center; gap:6px; } 6274 6275 .pa-filters-overlay { 6276 position:fixed; 6277 inset:0; 6278 background:rgba(0,20,45,.55); 6279 z-index:998; 6280 opacity:0; 6281 transition:opacity .35s; 6282 display:block; 6283 pointer-events:none; 6284 backdrop-filter:blur(2px); 6285 } 6286 .pa-filters-overlay.active { opacity:1; pointer-events:auto; } 6287 6288 .pa-filters { 6289 position:fixed; 6290 top:auto; 6291 left:0; right:0; bottom:0; 6292 max-height:92vh; 6293 height:auto; 6294 width:100%; 6295 border-radius:30px 30px 0 0; 6296 border:1px solid #dbe4ef; 6297 padding:26px 22px 0; 6298 z-index:10000; /* di atas bottom-nav (9998) */ 6299 transform:translateY(102%); 6300 transition:transform .55s cubic-bezier(.68,-0.35,.25,1); 6301 box-shadow:0 -4px 40px -6px rgba(15,23,42,.45); 6302 overflow:hidden; /* outer hidden, inner scroll */ 6303 } 6304 .pa-filters.active { transform:translateY(0); } 6305 6306 /* Drag handle indicator */ 6307 .pa-filters::after { 6308 content:""; 6309 display:block; 6310 position:absolute; 6311 top:10px; 6312 left:50%; 6313 transform:translateX(-50%); 6314 width:36px; 6315 height:4px; 6316 background:#cbd5e1; 6317 border-radius:4px; 6318 } 6319 body.filter-open { overflow:hidden; touch-action:none; } 6320 6321 /* Restore mobile-friendly input sizes (prevent iOS zoom on focus) */ 6322 .pa-filters input[type=text], 6323 .pa-filters input[type=number], 6324 .pa-filters select { 6325 font-size:16px; 6326 min-height:44px; 6327 padding:10px 12px; 6328 border-radius:10px; 6329 } 6330 .pa-filters select { padding-right:32px; } 6331 6332 .filters-scroll { 6333 flex:1; 6334 overflow:auto; 6335 padding:0 0 120px; /* ruang di atas bar actions */ 6336 margin:0; 6337 -webkit-overflow-scrolling:touch; 6338 } 6339 6340 .pa-filters h2 { font-size:19px; margin-bottom:10px; } 6341 6342 .pa-filters .close-drawer { 6343 display: block; 6344 position:absolute; 6345 right:14px; top:12px; 6346 background:linear-gradient(135deg,#dc2626 0%,#b91c1c 100%); 6347 color:#fff; 6348 padding:7px 14px; 6349 border:none; 6350 border-radius:10px; 6351 font-size:12px; 6352 font-weight:600; 6353 cursor:pointer; 6354 box-shadow:0 3px 10px -2px rgba(220,38,38,.55); 6355 transition:transform .25s, box-shadow .3s; 6356 z-index: 99; 6357 } 6358 .pa-filters .close-drawer:hover { transform:translateY(-2px); box-shadow:0 6px 18px -4px rgba(220,38,38,.6); } 6359 6360 .spec-attr { border-radius:18px; } 6361 .spec-attr-options label { font-size:11px; padding:5px 9px; } 6362 6363 /* Actions fixed di viewport bawah */ 6364 .f-actions { 6365 position:fixed; 6366 left:0; right:0; bottom:0; 6367 margin:0; 6368 border-top:1px solid #d6dee8; 6369 padding:14px 16px calc(16px + env(safe-area-inset-bottom)); 6370 background:#ffffffea; 6371 backdrop-filter:blur(6px); 6372 box-shadow:0 -4px 22px -6px rgba(15,23,42,.25); 6373 justify-content:space-between; 6374 z-index:10001; /* di atas pa-filters (10000) dan bottom-nav (9998) */ 6375 } 6376 6377 /* Sembunyikan bottom-nav saat filter drawer terbuka */ 6378 body.filter-open .bottom-nav { 6379 display: none !important; 6380 } 6381 .f-actions button { 6382 flex:1 1 auto; 6383 justify-content:center; 6384 padding:14px 14px; 6385 font-size:15px; 6386 border-radius:14px; 6387 } 6388 .f-actions .reset { 6389 order:-1; 6390 font-size:12px; 6391 padding:2px 8px; 6392 } 6393} 6394 6395/* Scrollbar (outer panel desktop only kept for compatibility) */ 6396.pa-filters::-webkit-scrollbar { width:0; } /* disembunyikan karena inner yang scroll */ 6397 6398/* Utilities */ 6399.hidden { display:none !important; } 6400.gradient-text { 6401 background:linear-gradient(90deg,#C9A227,#A16207,#C9A227); 6402 -webkit-background-clip:text; 6403 color:transparent; 6404 animation:titleSheen 10s linear infinite; 6405} 6406.glow-focus:focus {
6407 outline:none !important; 6408 box-shadow:0 0 0 3px rgba(201,162,39,.4) !important; 6409 border-color:#C9A227 !important; 6410} 6411 6412/* Simple fade (opsional) */ 6413@keyframes fadeIn { 6414 from { opacity:0; transform:translateY(8px); } 6415 to { opacity:1; transform:translateY(0); } 6416} 6417</style> 6418 <style data-inlined="mega-menu-tiles.css"> 6419/* Tile Mega Menu â Modern E-commerce Enhanced (2025) */ 6420 6421/* Design Tokens */ 6422:root { 6423 --mm2-bg: #ffffff; 6424 --mm2-surface: #ffffff; 6425 --mm2-surface-elevated: rgba(255,255,255,0.98); 6426 --mm2-muted: #f8fafc; 6427 --mm2-border: #e2e8f0; 6428 --mm2-border-strong: #cbd5e1; 6429 --mm2-accent: #B45309; 6430 --mm2-accent-hover: #78350F; 6431 --mm2-accent-10: rgba(99,102,241,.10); 6432 --mm2-accent-20: rgba(99,102,241,.20); 6433 --mm2-fg: #0f172a; 6434 --mm2-fg-muted: #64748b; 6435 --mm2-fg-light: #94a3b8; 6436 6437 /* Enhanced Shadows - Multi-layer elevation */ 6438 --mm2-shadow-sm: 0 2px 8px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04); 6439 --mm2-shadow-md: 0 8px 24px rgba(15,23,42,.10), 0 2px 8px rgba(15,23,42,.06); 6440 --mm2-shadow-lg: 0 20px 60px rgba(15,23,42,.14), 0 8px 24px rgba(15,23,42,.08); 6441 --mm2-shadow-xl: 0 32px 80px rgba(15,23,42,.18), 0 12px 32px rgba(15,23,42,.10); 6442 --mm2-shadow-hover: 0 16px 40px rgba(15,23,42,.16), 0 6px 16px rgba(15,23,42,.08); 6443 6444 /* Border Radius */ 6445 --mm2-radius-xl: 20px; 6446 --mm2-radius-lg: 16px; 6447 --mm2-radius-md: 12px; 6448 --mm2-radius-sm: 10px; 6449 6450 --mm2-z: 60; 6451 6452 /* Animation Timing */ 6453 --mm2-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 6454 --mm2-transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); 6455 --mm2-transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); 6456} 6457 6458.navbar { position: relative; z-index: var(--mm2-z); } 6459 6460/* Trigger Button - Enhanced */ 6461.mm2-trigger { 6462 display: inline-flex; align-items: center; gap: 8px; 6463 padding: 10px 14px; border-radius: var(--mm2-radius-md); 6464 border: 1.5px solid var(--mm2-border); 6465 background: var(--mm2-surface); color: var(--mm2-fg); 6466 cursor: pointer; user-select: none; 6467 transition: all var(--mm2-transition-fast); 6468 text-decoration: none; 6469 font-weight: 600; 6470 box-shadow: var(--mm2-shadow-sm); 6471} 6472.mm2-trigger:hover { 6473 border-color: var(--mm2-accent); 6474 background: var(--mm2-muted); 6475 box-shadow: var(--mm2-shadow-md); 6476 transform: translateY(-1px); 6477} 6478.mm2-trigger:active { transform: translateY(0); } 6479.mm2-trigger svg {max-width: unset !important; transition: transform var(--mm2-transition-fast); } 6480.mm2-trigger:hover svg { transform: scale(1.05); } 6481 6482/* Backdrop - Enhanced Glassmorphism */ 6483.mm2-backdrop { 6484 position: fixed; 6485 inset: 0; 6486 top: 0; 6487 left: 0; 6488 right: 0; 6489 bottom: 0; 6490 background: rgba(15,23,42,.40); 6491 backdrop-filter: blur(8px) saturate(1.2); 6492 -webkit-backdrop-filter: blur(8px) saturate(1.2); 6493 opacity: 0; 6494 pointer-events: none; 6495 z-index: 499; 6496 transition: opacity var(--mm2-transition-base); 6497} 6498.mm2-open .mm2-backdrop { opacity: 1; pointer-events: auto; } 6499 6500/* Panel - Glassmorphism Enhanced */ 6501.mm2-panel { 6502 position: fixed; 6503 left: 50%; 6504 top: 120px; 6505 transform: translateX(-50%) translateY(-16px); 6506 width: min(1200px, 95vw); 6507 background: var(--mm2-surface-elevated); 6508 backdrop-filter: blur(20px) saturate(1.8); 6509 -webkit-backdrop-filter: blur(20px) saturate(1.8); 6510 border: 1.5px solid var(--mm2-border-strong); 6511 border-radius: var(--mm2-radius-xl); 6512 box-shadow: var(--mm2-shadow-xl); 6513 display: none; 6514 overflow: hidden; 6515 opacity: 0; 6516 z-index: 500; 6517 transition: transform var(--mm2-transition-slow), opacity var(--mm2-transition-base); 6518} 6519.mm2-open .mm2-panel { 6520 display: block; 6521 opacity: 1; 6522 transform: translateX(-50%) translateY(0); 6523} 6524 6525/* View switcher */ 6526.mm2-view { display: none; } 6527.mm2-open .mm2-view.is-active { display: block; } 6528 6529/* Top grid (level-1 tiles) - Enhanced */ 6530.mm2-top { 6531 padding: 24px; 6532 max-height: 75vh; overflow: auto; 6533} 6534.mm2-grid { 6535 display: grid; 6536 grid-template-columns: repeat(4, minmax(0,1fr)); 6537 gap: 14px; 6538} 6539@media (max-width: 1200px) { .mm2-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } } 6540@media (max-width: 960px) { .mm2-grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; } } 6541 6542/* Stagger animation for cards */ 6543.mm2-open .mm2-card { 6544 animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) backwards; 6545} 6546.mm2-open .mm2-card:nth-child(1) { animation-delay: 0.02s; } 6547.mm2-open .mm2-card:nth-child(2) { animation-delay: 0.04s; } 6548.mm2-open .mm2-card:nth-child(3) { animation-delay: 0.06s; } 6549.mm2-open .mm2-card:nth-child(4) { animation-delay: 0.08s; } 6550.mm2-open .mm2-card:nth-child(5) { animation-delay: 0.10s; } 6551.mm2-open .mm2-card:nth-child(6) { animation-delay: 0.12s; } 6552.mm2-open .mm2-card:nth-child(7) { animation-delay: 0.14s; } 6553.mm2-open .mm2-card:nth-child(8) { animation-delay: 0.16s; } 6554.mm2-open .mm2-card:nth-child(9) { animation-delay: 0.18s; } 6555.mm2-open .mm2-card:nth-child(10) { animation-delay: 0.20s; } 6556.mm2-open .mm2-card:nth-child(11) { animation-delay: 0.22s; } 6557.mm2-open .mm2-card:nth-child(12) { animation-delay: 0.24s; } 6558.mm2-open .mm2-card:nth-child(n+13) { animation-delay: 0.26s; } 6559 6560@keyframes slideInUp { 6561 from { 6562 opacity: 0; 6563 transform: translateY(12px); 6564 } 6565 to { 6566 opacity: 1;
6567 transform: translateY(0); 6568 } 6569} 6570 6571.mm2-card { 6572 position: relative; 6573 display: flex; gap: 14px; align-items: center; 6574 padding: 16px; border-radius: var(--mm2-radius-md); 6575 background: var(--mm2-surface); 6576 border: 1.5px solid var(--mm2-border); 6577 text-decoration: none; color: var(--mm2-fg); 6578 transition: all var(--mm2-transition-fast); 6579 min-height: 72px; 6580 box-shadow: var(--mm2-shadow-sm); 6581} 6582.mm2-card:hover { 6583 border-color: var(--mm2-accent); 6584 background: linear-gradient(135deg, rgba(255,255,255,1), rgba(248,250,252,1)); 6585 box-shadow: var(--mm2-shadow-hover); 6586 transform: translateY(-3px); 6587} 6588.mm2-card:active { transform: translateY(-1px); } 6589 6590/* Icon with category-specific gradients */ 6591.mm2-card .mm2-ico { 6592 width: 48px; height: 48px; border-radius: 12px; 6593 display: inline-flex; align-items: center; justify-content: center; 6594 background: linear-gradient(135deg, #e0e7ff, #fff); 6595 border: 1.5px solid #c7d2fe; 6596 color: #78350F; 6597 flex-shrink: 0; 6598 transition: all var(--mm2-transition-fast); 6599 box-shadow: 0 2px 8px rgba(99,102,241,0.1); 6600} 6601.mm2-card:hover .mm2-ico { 6602 transform: scale(1.08) rotate(2deg); 6603 box-shadow: 0 4px 16px rgba(99,102,241,0.2); 6604} 6605.mm2-card .mm2-ico svg { transition: transform var(--mm2-transition-fast); } 6606.mm2-card:hover .mm2-ico svg { transform: scale(1.1); } 6607 6608.mm2-card .mm2-title { 6609 font-weight: 700; 6610 font-size: 15px; 6611 letter-spacing: -0.01em; 6612 line-height: 1.3; 6613} 6614.mm2-card .mm2-sub { 6615 color: var(--mm2-fg-muted); 6616 font-size: 12.5px; 6617 margin-top: 3px; 6618 display: flex; 6619 align-items: center; 6620 gap: 4px; 6621 white-space: nowrap; 6622 overflow: hidden; 6623 text-overflow: ellipsis; 6624} 6625.mm2-card .mm2-count { 6626 display: inline-flex; 6627 padding: 2px 8px; 6628 border-radius: 6px; 6629 background: var(--mm2-accent-10); 6630 color: var(--mm2-accent); 6631 font-weight: 600; 6632 font-size: 11px; 6633 margin-left: 4px; 6634} 6635 6636/* Detail view (level-2 + level-3) - Enhanced */ 6637.mm2-detail { 6638 position: relative; 6639 padding: 16px 24px 24px; 6640 max-height: 70vh; overflow: auto; 6641} 6642.mm2-detail-header { 6643 display: flex; align-items: center; gap: 14px; 6644 padding: 10px 0 16px; border-bottom: 2px solid var(--mm2-muted); margin-bottom: 16px; 6645} 6646.mm2-back { 6647 display: inline-flex; align-items: center; justify-content: center; 6648 width: 40px; height: 40px; border-radius: 12px; 6649 border: 1.5px solid var(--mm2-border); background: #fff; color: var(--mm2-fg); 6650 cursor: pointer; 6651 transition: all var(--mm2-transition-fast); 6652 box-shadow: var(--mm2-shadow-sm); 6653} 6654.mm2-back:hover { 6655 border-color: var(--mm2-accent); 6656 background: var(--mm2-muted); 6657 transform: translateX(-3px); 6658 box-shadow: var(--mm2-shadow-md); 6659} 6660.mm2-detail-title { 6661 font-weight: 800; 6662 font-size: 18px; 6663 letter-spacing: -0.02em; 6664} 6665 6666/* Two-pane layout (desktop) - Enhanced */ 6667.mm2-detail-layout { 6668 display: grid; 6669 grid-template-columns: 280px 1fr; 6670 gap: 20px; 6671} 6672 6673/* Left: subnav (level-2 list) - Enhanced */ 6674.mm2-subnav { 6675 position: sticky; top: 8px; 6676 align-self: start; 6677 max-height: calc(70vh - 56px); 6678 overflow: auto; 6679 padding-right: 6px; 6680} 6681.mm2-subnav::-webkit-scrollbar { width: 8px; height: 8px; } 6682.mm2-subnav::-webkit-scrollbar-track { background: transparent; } 6683.mm2-subnav::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; } 6684.mm2-subnav::-webkit-scrollbar-thumb:hover { background: #94a3b8; } 6685 6686.mm2-subitem { 6687 display: flex; align-items: center; justify-content: flex-start; gap: 10px; 6688 width: 100%; 6689 padding: 12px 14px; 6690 border-radius: 12px; 6691 border: 1.5px solid transparent; 6692 background: transparent; 6693 color: var(--mm2-fg); 6694 cursor: pointer; 6695 text-align: left; 6696 transition: all var(--mm2-transition-fast); 6697 margin-bottom: 6px; 6698 font-weight: 600; 6699 font-size: 14px; 6700 position: relative; 6701} 6702.mm2-subitem::before { 6703 content: ''; 6704 position: absolute; 6705 left: 0; 6706 top: 50%; 6707 transform: translateY(-50%); 6708 width: 3px; 6709 height: 0; 6710 background: var(--mm2-accent); 6711 border-radius: 0 2px 2px 0; 6712 transition: height var(--mm2-transition-fast); 6713} 6714.mm2-subitem:hover { 6715 background: var(--mm2-muted); 6716 color: var(--mm2-accent-hover); 6717} 6718.mm2-subitem.active { 6719 background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(99,102,241,0.04)); 6720 border-color: var(--mm2-accent-20); 6721 color: var(--mm2-accent-hover); 6722 box-shadow: inset 0 0 0 1px rgba(99,102,241,0.1); 6723} 6724.mm2-subitem.active::before { 6725 height: 24px; 6726} 6727 6728/* Right: subcontent (level-3 grid) - Enhanced */ 6729.mm2-subcontent { min-height: 240px; } 6730.mm2-links { 6731 display: grid; gap: 12px 14px; 6732 grid-template-columns: repeat(3, minmax(0,1fr)); 6733} 6734@media (max-width: 1200px) { .mm2-links { grid-template-columns: repeat(2, minmax(0,1fr)); } } 6735 6736.mm2-link { 6737 display: flex; align-items: center; gap: 12px; 6738 padding: 14px; border: 1.5px solid var(--mm2-border); border-radius: var(--mm2-radius-md); 6739 background: var(--mm2-surface); color: var(--mm2-fg); text-decoration: none; 6740 min-height: 52px; 6741 font-weight: 500; 6742 font-size: 14px; 6743 transition: all var(--mm2-transition-fast); 6744 box-shadow: var(--mm2-shadow-sm); 6745 position: relative; 6746 justify-content: space-between; 6747} 6748.mm2-link:hover { 6749 background: var(--mm2-muted); 6750 border-color: var(--mm2-accent); 6751 transform: translateX(4px); 6752 box-shadow: var(--mm2-shadow-md); 6753} 6754.mm2-link-text { 6755 flex: 1; 6756} 6757 6758/* Product Thumbnails */ 6759.mm2-thumbs { 6760 display: flex; 6761 gap: 6px; 6762 align-items: center; 6763 margin-left: auto; 6764 flex-shrink: 0; 6765} 6766.mm2-thumb { 6767 width: 36px; 6768 height: 36px; 6769 border-radius: 8px; 6770 object-fit: cover; 6771 border: 1.5px solid var(--mm2-border); 6772 box-shadow: 0 2px 6px rgba(0,0,0,0.08); 6773 transition: all var(--mm2-transition-fast); 6774 background: var(--mm2-muted); 6775} 6776.mm2-link:hover .mm2-thumb { 6777 border-color: var(--mm2-accent); 6778 box-shadow: 0 4px 10px rgba(99,102,241,0.15); 6779 transform: scale(1.08); 6780} 6781
6782/* Thumbnail Skeleton Loader */ 6783.mm2-thumb-skeleton { 6784 width: 36px; 6785 height: 36px; 6786 border-radius: 8px; 6787 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 6788 background-size: 200% 100%; 6789 animation: shimmer 1.5s infinite; 6790 border: 1.5px solid var(--mm2-border); 6791} 6792 6793.mm2-badge { 6794 font-size: 11px; padding: 3px 10px; margin-left: auto; 6795 border-radius: 12px; background: var(--mm2-accent-10); color: var(--mm2-accent); 6796 font-weight: 700; 6797 border: 1px solid var(--mm2-accent-20); 6798} 6799 6800/* "See all" highlight card - Enhanced */ 6801.mm2-seeall { 6802 border: 2px dashed var(--mm2-accent); 6803 background: linear-gradient(135deg, rgba(99,102,241,.08), rgba(99,102,241,.02)); 6804 font-weight: 700; 6805} 6806.mm2-seeall:hover { 6807 background: linear-gradient(135deg, rgba(99,102,241,.14), rgba(99,102,241,.06)); 6808 border-style: solid; 6809 transform: translateX(6px) scale(1.02); 6810} 6811 6812/* Mobile panel styles - Enhanced with drag handle & better touch targets */ 6813@media (max-width: 960px) { 6814 .mm2-panel { 6815 position: fixed; 6816 left: 0; 6817 right: 0; 6818 bottom: 0; 6819 top: auto; 6820 width: 100%; 6821 max-height: 88vh; 6822 transform: translateX(0) translateY(100%); 6823 border-radius: var(--mm2-radius-xl) var(--mm2-radius-xl) 0 0; 6824 border-bottom: none; 6825 } 6826 .mm2-open .mm2-panel { 6827 transform: translateX(0) translateY(0); z-index: 9999; 6828 } 6829 6830 /* Enhanced backdrop for mobile */ 6831 .mm2-backdrop { 6832 backdrop-filter: blur(6px) saturate(1.2); 6833 -webkit-backdrop-filter: blur(6px) saturate(1.2); 6834 } 6835 6836 /* Mobile close button - Soft glassmorphism */ 6837 .mm2-mobile-close { 6838 position: absolute; 6839 top: 4px; 6840 right: 8px; 6841 width: 28px; 6842 height: 28px; 6843 border-radius: 50%; 6844 border: 1px solid rgba(226, 232, 240, 0.6); 6845 background: rgba(255, 255, 255, 0.85); 6846 backdrop-filter: blur(12px) saturate(1.8); 6847 -webkit-backdrop-filter: blur(12px) saturate(1.8); 6848 color: var(--mm2-fg-muted); 6849 display: inline-flex;
6850 align-items: center; 6851 justify-content: center; 6852 cursor: pointer; 6853 transition: all var(--mm2-transition-fast); 6854 box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); 6855 z-index: 20; 6856 } 6857 .mm2-mobile-close:hover { 6858 background: rgba(254, 242, 242, 0.95); 6859 border-color: rgba(252, 165, 165, 0.8); 6860 color: #dc2626; 6861 box-shadow: 0 4px 12px rgba(220, 38, 38, 0.15); 6862 } 6863 .mm2-mobile-close:active { 6864 transform: scale(0.92); 6865 } 6866 6867 /* Drag handle indicator */ 6868 .mm2-top::before, 6869 .mm2-detail::before { 6870 content: ''; 6871 position: sticky; 6872 top: 0; 6873 left: 50%; 6874 transform: translateX(-50%); 6875 width: 40px; 6876 height: 4px; 6877 background: var(--mm2-border-strong); 6878 border-radius: 999px; 6879 margin: 12px auto 20px; 6880 display: block; 6881 z-index: 10; 6882 transition: background var(--mm2-transition-fast); 6883 } 6884 .mm2-panel:active .mm2-top::before, 6885 .mm2-panel:active .mm2-detail::before { 6886 background: #94a3b8; 6887 } 6888 6889 .mm2-top { 6890 padding: 10px; 6891 } 6892 6893 .mm2-top, .mm2-detail { 6894 max-height: min(82vh, 720px); 6895 padding-top: 8px; 6896 } 6897 6898 /* Two-pane collapses: subnav becomes GRID (no horizontal scroll) */ 6899 .mm2-detail-layout { display: block; } 6900 .mm2-subnav { 6901 position: static; max-height: none; overflow: visible; 6902 display: grid; gap: 10px; padding: 10px 4px 16px; margin-bottom: 16px; 6903 grid-template-columns: repeat(2, minmax(0,1fr)); 6904 border-bottom: 2px solid var(--mm2-muted); 6905 } 6906 .mm2-subitem { 6907 margin: 0; width: 100%; text-align: center; 6908 border-radius: 14px; 6909 background: #fff; border: 1.5px solid var(--mm2-border); 6910 white-space: normal; line-height: 1.3; min-height: 48px; 6911 justify-content: center; 6912 padding: 12px 10px; 6913 font-size: 13.5px; 6914 box-shadow: var(--mm2-shadow-sm); 6915 } 6916 .mm2-subitem::before { display: none; } 6917 .mm2-subitem:hover { 6918 transform: scale(1.02); 6919 } 6920 .mm2-subitem.active { 6921 background: linear-gradient(135deg, var(--mm2-accent), var(--mm2-accent-hover)); 6922 border-color: var(--mm2-accent-hover); 6923 color: white; 6924 box-shadow: 0 4px 12px rgba(99,102,241,0.3); 6925 font-weight: 700; 6926 } 6927 6928 /* Level-3 grid tetap 2 kolom dengan spacing lebih baik */ 6929 .mm2-links { 6930 grid-template-columns: repeat(2, minmax(0,1fr)); 6931 gap: 10px; 6932 } 6933 6934 /* Mobile: hide thumbnails untuk save space */ 6935 .mm2-thumbs { 6936 display: none; 6937 } 6938 6939 /* Larger touch targets for mobile */ 6940 .mm2-card { 6941 min-height: 80px; 6942 padding: 18px 14px; 6943 } 6944 .mm2-card .mm2-ico { 6945 width: 50px; 6946 height: 50px; 6947 } 6948 .mm2-card .mm2-title { 6949 font-size: 14px; 6950 } 6951 .mm2-card .mm2-sub { 6952 font-size: 11.5px; 6953 } 6954 .mm2-link { 6955 min-height: 56px; 6956 padding: 16px; 6957 } 6958 .mm2-back { 6959 width: 44px; 6960 height: 44px; 6961 } 6962 6963 /* Grid spacing */ 6964 .mm2-grid { 6965 gap: 10px; 6966 } 6967} 6968</style> 6969 <style data-inlined="product-compare.css"> 6970/* Product Compare Feature Styles */ 6971 6972/* Compare Button in Product Detail - Modern Chip Style */ 6973.pd-compare-section { 6974 margin: 0.5rem 0; 6975 display: flex; 6976 align-items: center; 6977 gap: 0.5rem; 6978} 6979 6980.pd-compare-btn { 6981 display: inline-flex; 6982 align-items: center; 6983 gap: 0.4rem; 6984 padding: 0.5rem 0.85rem; 6985 border: 1px solid #cbd5e1; 6986 border-radius: 20px; 6987 background: #fff; 6988 color: #64748b; 6989 font-weight: 600; 6990 font-size: 0.8rem; 6991 cursor: pointer; 6992 transition: all 0.2s ease; 6993 position: relative; 6994 white-space: nowrap; 6995} 6996 6997.pd-compare-btn:hover { 6998 border-color: #EAB308; 6999 color: #EAB308; 7000 background: #FFFBEB; 7001 transform: translateY(-1px); 7002 box-shadow: 0 2px 6px rgba(234, 179, 8, 0.15); 7003} 7004 7005.pd-compare-btn.added { 7006 border-color: #16a34a; 7007 color: #16a34a; 7008 background: #f0fdf4; 7009} 7010 7011.pd-compare-btn .compare-remove { 7012 display: none; 7013} 7014 7015.pd-compare-btn.added .compare-text { 7016 display: none; 7017} 7018 7019.pd-compare-btn.added .compare-remove { 7020 display: flex !important; 7021 align-items: center; 7022 gap: 0.25rem; 7023 font-weight: 600; 7024 font-size: 0.9rem; 7025 color: #dc2626 !important; 7026 opacity: 1 !important; 7027 visibility: visible !important; 7028 z-index: 1; 7029 position: relative; 7030} 7031 7032.pd-compare-btn.added .compare-remove[hidden] { 7033 display: flex !important; 7034} 7035 7036/* Force visibility for added state */ 7037.pd-compare-btn.added .compare-remove { 7038 display: flex !important; 7039} 7040 7041[hidden].compare-remove { 7042 display: none !important; 7043} 7044 7045.pd-compare-btn.added [hidden].compare-remove { 7046 display: flex !important; 7047} 7048 7049/* Ensure the added state text is always visible */ 7050.pd-compare-btn.added { 7051 border-color: #dc2626 !important; 7052 color: #dc2626 !important; 7053 background: #fef2f2 !important; 7054} 7055 7056.pd-compare-btn.added .compare-remove svg { 7057 color: #dc2626 !important; 7058} 7059 7060.pd-compare-btn.added:hover { 7061 border-color: #15803d; 7062 color: #15803d; 7063 background: #dcfce7; 7064 transform: translateY(-1px); 7065 box-shadow: 0 2px 6px rgba(22, 163, 74, 0.2); 7066} 7067 7068.pd-compare-btn .compare-icon { 7069 width: 16px; 7070 height: 16px; 7071 flex-shrink: 0; 7072} 7073 7074.pd-compare-info { 7075 display: inline-flex; 7076 align-items: center; 7077} 7078 7079.compare-note { 7080 color: #94a3b8; 7081 font-size: 0.7rem; 7082 font-weight: 500; 7083 transition: color 0.2s ease; 7084} 7085 7086.pd-compare-btn.added + .pd-compare-info .compare-note { 7087 color: #16a34a; 7088 font-weight: 600; 7089} 7090 7091/* Compare Badge in Navigation */ 7092.compare-indicator { 7093 position: relative; 7094} 7095 7096.compare-indicator .badge { 7097 position: absolute; 7098 top: -8px; 7099 right: -8px; 7100 min-width: 18px;
7101 height: 18px; 7102 padding: 0 5px; 7103 background: #ef4444; 7104 color: white; 7105 border-radius: 9px; 7106 font-size: 11px; 7107 7108 7109/* Floating Compare Badge (Sticky) */ 7110.compare-floating-badge { 7111 position: fixed; 7112 right: 16px; 7113 bottom: calc(20px + 60px); /* Above scroll-to-top button */ 7114 width: auto; 7115 min-width: 56px; 7116 height: 56px; 7117 padding: 0 16px; 7118 border-radius: 28px; 7119 background: linear-gradient(135deg, #EAB308 0%, #CA8A04 100%); 7120 color: #fff; 7121 box-shadow: 0 8px 24px -8px rgba(234, 179, 8, 0.5); 7122 cursor: pointer; 7123 z-index: 1104; 7124 opacity: 0; 7125 transform: translateY(8px) scale(0.9); 7126 transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.2s ease; 7127 display: flex; 7128 align-items: center; 7129 justify-content: center; 7130 gap: 8px; 7131 font-weight: 600; 7132 font-size: 0.85rem; 7133 pointer-events: none; 7134} 7135 7136.compare-floating-badge.visible { 7137 opacity: 1; 7138 transform: translateY(0) scale(1); 7139 pointer-events: auto; 7140} 7141 7142.compare-floating-badge:hover { 7143 box-shadow: 0 12px 32px -10px rgba(234, 179, 8, 0.6); 7144 transform: translateY(-2px) scale(1.02); 7145} 7146 7147.compare-floating-badge:active { 7148 transform: translateY(0) scale(0.98); 7149} 7150 7151.compare-floating-badge .cfb-icon { 7152 width: 20px; 7153 height: 20px; 7154 flex-shrink: 0; 7155} 7156 7157.compare-floating-badge .cfb-count { 7158 font-size: 0.9rem; 7159 font-weight: 700; 7160 min-width: 20px; 7161 text-align: center; 7162 background: rgba(255, 255, 255, 0.25); 7163 padding: 2px 8px; 7164 border-radius: 12px; 7165 line-height: 1; 7166 display: flex; 7167 align-items: center; 7168 justify-content: center; 7169 height: 24px; 7170} 7171 7172.compare-floating-badge .cfb-text { 7173 display: none; 7174} 7175 7176@media (min-width: 641px) { 7177 .compare-floating-badge .cfb-text { 7178 display: block; 7179 white-space: nowrap; 7180 } 7181} 7182 7183@media (max-width: 640px) { 7184 .compare-floating-badge { 7185 right: 14px; 7186 bottom: calc(var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))) + 60px); 7187 min-width: 48px; 7188 height: 48px; 7189 padding: 0 12px; 7190 border-radius: 24px; 7191 } 7192 7193 .compare-floating-badge .cfb-icon { 7194 width: 18px; 7195 height: 18px; 7196 } 7197 7198 .compare-floating-badge .cfb-count { 7199 font-size: 0.8rem; 7200 padding: 2px 6px; 7201 height: 20px; 7202 } 7203} font-weight: 600; 7204 display: flex; 7205 align-items: center; 7206 justify-content: center; 7207 transform: scale(0); 7208 transition: transform 0.2s ease; 7209} 7210 7211.compare-indicator .badge.active { 7212 transform: scale(1); 7213} 7214 7215/* Compare Sidebar */ 7216.compare-sidebar { 7217 position: fixed; 7218 top: 0; 7219 right: -400px; 7220 width: 400px; 7221 height: 100vh; 7222 background: #ffffff; 7223 box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); 7224 z-index: 9999; 7225 display: flex; 7226 flex-direction: column; 7227 transition: right 0.3s ease; 7228} 7229 7230.compare-sidebar.open { 7231 right: 0; 7232} 7233 7234.compare-overlay { 7235 position: fixed; 7236 top: 0; 7237 left: 0; 7238 width: 100%; 7239 height: 100%; 7240 background: rgba(0, 0, 0, 0.5); 7241 z-index: 9998; 7242 opacity: 0; 7243 visibility: hidden; 7244 transition: all 0.3s ease; 7245} 7246 7247.compare-overlay.active { 7248 opacity: 1; 7249 visibility: visible; 7250} 7251 7252.compare-header { 7253 display: flex; 7254 align-items: center; 7255 justify-content: space-between; 7256 padding: 1.25rem; 7257 border-bottom: 1px solid #e2e8f0; 7258 background: #f8fafc; 7259} 7260 7261.compare-title { 7262 display: flex; 7263 align-items: center; 7264 gap: 0.5rem; 7265 font-size: 1rem; 7266 font-weight: 600; 7267 color: #1e293b; 7268 margin: 0; 7269} 7270 7271.compare-count { 7272 color: #64748b; 7273 font-weight: 400; 7274} 7275 7276.compare-close { 7277 display: flex; 7278 align-items: center; 7279 justify-content: center; 7280 width: 32px; 7281 height: 32px; 7282 border: none; 7283 border-radius: 6px; 7284 background: transparent; 7285 color: #64748b; 7286 cursor: pointer; 7287 transition: all 0.2s ease; 7288} 7289 7290.compare-close:hover { 7291 background: #e2e8f0; 7292 color: #374151; 7293} 7294 7295.compare-content { 7296 flex: 1; 7297 overflow-y: auto; 7298 padding: 1rem; 7299} 7300 7301.compare-empty { 7302 display: flex; 7303 flex-direction: column; 7304 align-items: center; 7305 justify-content: center; 7306 height: 100%; 7307 text-align: center; 7308 padding: 2rem 1rem; 7309} 7310 7311.compare-empty h5 { 7312 margin: 1rem 0 0.5rem; 7313 color: #374151; 7314} 7315 7316.compare-empty p { 7317 color: #64748b; 7318 font-size: 0.875rem; 7319 line-height: 1.5; 7320} 7321 7322.compare-list { 7323 display: flex; 7324 flex-direction: column; 7325 gap: 0.75rem; 7326} 7327 7328.compare-item { 7329 display: flex; 7330 align-items: center; 7331 gap: 0.75rem; 7332 padding: 0.75rem; 7333 border: 1px solid #e2e8f0; 7334 border-radius: 8px; 7335 background: #ffffff; 7336 transition: all 0.2s ease; 7337} 7338 7339.compare-item:hover { 7340 border-color: #cbd5e1; 7341 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 7342} 7343 7344.compare-item-image { 7345 flex-shrink: 0; 7346 width: 48px; 7347 height: 48px; 7348 border-radius: 6px; 7349 overflow: hidden; 7350 border: 1px solid #e2e8f0; 7351} 7352 7353.compare-item-image img { 7354 width: 100%; 7355 height: 100%; 7356 object-fit: cover; 7357} 7358 7359.compare-item-info { 7360 flex: 1; 7361 min-width: 0; 7362} 7363 7364.compare-item-name { 7365 font-weight: 500; 7366 font-size: 0.875rem; 7367 color: #1e293b; 7368 margin-bottom: 0.25rem; 7369 line-height: 1.3; 7370 display: -webkit-box; 7371 -webkit-line-clamp: 2; 7372 -webkit-box-orient: vertical; 7373 overflow: hidden; 7374} 7375 7376.compare-item-price { 7377 font-weight: 600; 7378 font-size: 0.8rem; 7379 color: #059669; 7380} 7381 7382.compare-item-remove { 7383 flex-shrink: 0; 7384 display: flex; 7385 align-items: center; 7386 justify-content: center; 7387 width: 28px; 7388 height: 28px; 7389 border: none; 7390 border-radius: 4px; 7391 background: transparent; 7392 color: #dc2626; 7393 cursor: pointer; 7394 transition: all 0.2s ease; 7395} 7396 7397.compare-item-remove:hover { 7398 background: #fef2f2; 7399 color: #991b1b; 7400} 7401 7402.compare-actions { 7403 padding: 1rem; 7404 border-top: 1px solid #e2e8f0; 7405 display: flex; 7406 gap: 0.75rem; 7407} 7408 7409.compare-actions .btn { 7410 flex: 1; 7411 padding: 0.75rem 1rem; 7412 border-radius: 6px; 7413 font-weight: 600; 7414 font-size: 0.875rem; 7415 display: flex; 7416 align-items: center; 7417 justify-content: center; 7418 gap: 0.5rem; 7419 cursor: pointer; 7420 transition: all 0.2s ease; 7421} 7422 7423.btn-outline-danger { 7424 border: 1px solid #dc2626; 7425 background: transparent; 7426 color: #dc2626; 7427} 7428 7429.btn-outline-danger:hover { 7430 background: #dc2626; 7431 color: white; 7432} 7433 7434.btn-primary { 7435 border: 1px solid #EAB308; 7436 background: #EAB308; 7437 color: white; 7438} 7439 7440.btn-primary:hover { 7441 background: #CA8A04; 7442 border-color: #CA8A04; 7443} 7444 7445.btn-primary:disabled { 7446 opacity: 0.5;
7447 cursor: not-allowed; 7448} 7449 7450.btn-primary:disabled:hover { 7451 background: #EAB308; 7452 border-color: #EAB308; 7453} 7454 7455/* AI Comparison Modal */ 7456.ai-compare-modal { 7457 position: fixed; 7458 top: 0; 7459 left: 0; 7460 width: 100%; 7461 height: 100%; 7462 z-index: 10000; 7463 display: none; 7464 align-items: center; 7465 justify-content: center; 7466} 7467 7468.ai-compare-modal.open { 7469 display: flex; 7470} 7471 7472.ai-compare-modal .modal-backdrop { 7473 position: absolute; 7474 top: 0; 7475 left: 0; 7476 width: 100%; 7477 height: 100%; 7478 background: rgba(0, 0, 0, 0.6); 7479 backdrop-filter: blur(4px); 7480} 7481 7482.modal-container { 7483 position: relative; 7484 width: 90%; 7485 max-width: 1000px; 7486 max-height: 90vh; 7487 background: #ffffff; 7488 border-radius: 12px; 7489 box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); 7490 display: flex; 7491 flex-direction: column; 7492 overflow: hidden; 7493} 7494 7495.modal-header { 7496 display: flex; 7497 align-items: center; 7498 justify-content: space-between; 7499 padding: 1.5rem 2rem; 7500 border-bottom: 1px solid #e2e8f0; 7501 background: #f8fafc; 7502} 7503 7504.modal-title { 7505 display: flex; 7506 align-items: center; 7507 gap: 0.75rem; 7508 font-size: 1.25rem; 7509 font-weight: 600; 7510 color: #1e293b; 7511 margin: 0; 7512} 7513 7514.modal-close { 7515 display: flex; 7516 align-items: center; 7517 justify-content: center; 7518 width: 36px; 7519 height: 36px; 7520 border: none; 7521 border-radius: 8px; 7522 background: transparent; 7523 color: #64748b; 7524 cursor: pointer; 7525 transition: all 0.2s ease; 7526} 7527 7528.modal-close:hover { 7529 background: #e2e8f0; 7530 color: #374151; 7531} 7532 7533.modal-content { 7534 flex: 1; 7535 overflow-y: auto; 7536 padding: 2rem; 7537} 7538 7539/* AI Loading State */ 7540.ai-loading { 7541 display: flex; 7542 flex-direction: column; 7543 align-items: center; 7544 justify-content: center; 7545 padding: 3rem 2rem; 7546 text-align: center; 7547} 7548 7549.loading-animation { 7550 margin-bottom: 1rem; 7551} 7552 7553.typing-indicator { 7554 display: flex; 7555 align-items: center; 7556 justify-content: center; 7557 gap: 4px; 7558 margin-bottom: 1rem; 7559} 7560 7561.typing-indicator span { 7562 width: 8px; 7563 height: 8px; 7564 background: #EAB308; 7565 border-radius: 50%; 7566 animation: typing 1.4s infinite ease-in-out; 7567} 7568 7569.typing-indicator span:nth-child(2) { 7570 animation-delay: 0.2s; 7571} 7572 7573.typing-indicator span:nth-child(3) { 7574 animation-delay: 0.4s; 7575} 7576 7577@keyframes typing { 7578 0%, 80%, 100% { 7579 opacity: 0.3; 7580 transform: scale(0.8); 7581 } 7582 40% { 7583 opacity: 1; 7584 transform: scale(1); 7585 } 7586} 7587 7588.ai-loading p { 7589 font-weight: 500; 7590 color: #1e293b; 7591 margin-bottom: 0.5rem; 7592} 7593 7594.ai-loading small { 7595 color: #64748b; 7596} 7597 7598/* AI Error State */ 7599.ai-error { 7600 display: flex; 7601 flex-direction: column; 7602 align-items: center; 7603 justify-content: center; 7604 padding: 3rem 2rem; 7605 text-align: center; 7606} 7607 7608.ai-error h4 { 7609 margin: 1rem 0 0.5rem; 7610 color: #dc2626; 7611} 7612 7613.ai-error p { 7614 color: #64748b; 7615 margin-bottom: 1.5rem; 7616} 7617 7618/* AI Results */ 7619.ai-results > div { 7620 margin-bottom: 2rem; 7621} 7622 7623.ai-summary-card, 7624.ai-comparison-table, 7625.ai-detailed-analysis, 7626.ai-recommendations, 7627.ai-tips { 7628 background: #f8fafc; 7629 border: 1px solid #e2e8f0; 7630 border-radius: 12px; 7631 padding: 1.5rem; 7632} 7633 7634.ai-results h4 { 7635 display: flex; 7636 align-items: center; 7637 gap: 0.5rem; 7638 font-size: 1.1rem; 7639 font-weight: 600; 7640 color: #1e293b; 7641 margin-bottom: 1rem; 7642} 7643 7644.summary-text { 7645 font-size: 1rem; 7646 line-height: 1.6; 7647 color: #374151; 7648 margin-bottom: 1rem; 7649} 7650 7651.winner-badge { 7652 padding: 1rem; 7653 background: linear-gradient(135deg, #10b981 0%, #059669 100%); 7654 color: white; 7655 border-radius: 8px; 7656} 7657 7658.winner-badge strong { 7659 display: block; 7660 margin-bottom: 0.25rem; 7661} 7662 7663.winner-badge small { 7664 opacity: 0.9; 7665 font-size: 0.875rem; 7666} 7667 7668/* Comparison Table */ 7669.comparison-grid { 7670 display: grid; 7671 gap: 1rem; 7672 margin-top: 1rem; 7673} 7674 7675.comparison-category { 7676 background: white; 7677 border: 1px solid #e2e8f0; 7678 border-radius: 8px; 7679 overflow: hidden; 7680} 7681 7682.category-header { 7683 background: #f1f5f9; 7684 padding: 0.75rem 1rem; 7685 font-weight: 600; 7686 color: #1e293b; 7687 border-bottom: 1px solid #e2e8f0; 7688} 7689 7690.category-products { 7691 display: grid; 7692 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 7693 gap: 1px; 7694 background: #e2e8f0; 7695} 7696 7697.product-score { 7698 background: white; 7699 padding: 1rem; 7700 text-align: center; 7701} 7702 7703.score-value { 7704 display: inline-flex; 7705 align-items: center; 7706 gap: 0.25rem; 7707 margin-bottom: 0.5rem; 7708} 7709 7710.score-stars { 7711 color: #EAB308; 7712} 7713 7714.score-note { 7715 font-size: 0.8rem; 7716 color: #64748b; 7717 line-height: 1.3; 7718} 7719 7720/* Product name header in comparison table */ 7721.product-name-header { 7722 margin-bottom: 0.5rem; 7723} 7724 7725.product-name-header strong { 7726 font-size: .8rem; /* h5 size (18px) */ 7727 font-weight: 600; 7728 color: #1e293b; 7729 line-height: 1.3; 7730 display: block; 7731 word-break: break-word; 7732} 7733 7734.product-price { 7735 font-size: 0.8rem; 7736 font-weight: 600; 7737 color: #059669; 7738} 7739 7740/* Detailed Analysis */ 7741.products-analysis { 7742 display: grid; 7743 gap: 1rem; 7744 margin-top: 1rem; 7745} 7746 7747.product-analysis { 7748 background: white; 7749 border: 1px solid #e2e8f0; 7750 border-radius: 8px; 7751 padding: 1rem; 7752} 7753 7754.product-analysis h5 { 7755 color: #1e293b; 7756 margin-bottom: 0.75rem; 7757 font-weight: 600; 7758} 7759 7760.analysis-section { 7761 margin-bottom: 0.75rem; 7762} 7763 7764.analysis-section:last-child { 7765 margin-bottom: 0; 7766} 7767 7768.analysis-section h6 { 7769 font-size: 0.875rem; 7770 font-weight: 600; 7771 margin-bottom: 0.5rem; 7772} 7773 7774.analysis-section h6.strengths { 7775 color: #059669; 7776} 7777 7778.analysis-section h6.weaknesses { 7779 color: #dc2626; 7780} 7781 7782.analysis-section h6.best-for { 7783 color: #8B6914; 7784} 7785 7786.analysis-section ul { 7787 list-style: none; 7788 padding: 0; 7789 margin: 0; 7790} 7791 7792.analysis-section li { 7793 display: flex; 7794 align-items: flex-start; 7795 gap: 0.5rem; 7796 margin-bottom: 0.25rem; 7797 font-size: 0.875rem; 7798 line-height: 1.4; 7799} 7800 7801.analysis-section li:before { 7802 content: "â¢"; 7803 color: #64748b; 7804 font-weight: bold; 7805 margin-top: 0.1rem; 7806} 7807 7808.best-for-text { 7809 font-size: 0.875rem; 7810 color: #374151; 7811 font-style: italic; 7812} 7813 7814/* Recommendations */ 7815.recommendations-grid { 7816 display: grid; 7817 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 7818 gap: 1rem; 7819 margin-top: 1rem; 7820} 7821 7822.recommendation-card { 7823 background: white; 7824 border: 1px solid #e2e8f0; 7825 border-radius: 8px; 7826 padding: 1rem; 7827 text-align: center; 7828} 7829 7830.recommendation-card h5 { 7831 font-size: 1rem; 7832 margin-bottom: 0.75rem; 7833 color: #1e293b; 7834} 7835 7836.recommendation-card p { 7837 font-size: 0.875rem; 7838 color: #374151; 7839 line-height: 1.5; 7840} 7841 7842/* Tips */ 7843.tips-list { 7844 list-style: none; 7845 padding: 0; 7846 margin: 1rem 0 0 0; 7847} 7848 7849.tips-list li { 7850 display: flex; 7851 align-items: flex-start; 7852 gap: 0.75rem; 7853 margin-bottom: 0.75rem; 7854 font-size: 0.9rem; 7855 line-height: 1.5; 7856 color: #374151; 7857} 7858 7859.tips-list li:before { 7860 content: "ð¡"; 7861 font-size: 1rem; 7862 margin-top: 0.1rem; 7863} 7864 7865/* Modal Footer */ 7866.modal-footer { 7867 display: flex; 7868 justify-content: space-between; 7869 align-items: center; 7870 padding: 1.5rem 2rem; 7871 border-top: 1px solid #e2e8f0; 7872 background: #f8fafc; 7873} 7874 7875.btn-outline-secondary { 7876 border: 1px solid #64748b; 7877 background: transparent; 7878 color: #64748b; 7879 padding: 0.75rem 1.5rem; 7880 border-radius: 6px; 7881 font-weight: 600; 7882 display: flex; 7883 align-items: center; 7884 gap: 0.5rem; 7885 cursor: pointer; 7886 transition: all 0.2s ease; 7887} 7888 7889.btn-outline-secondary:hover { 7890 background: #64748b; 7891 color: white; 7892} 7893 7894/* Responsive for chip button */ 7895@media (max-width: 880px) { 7896 .pd-compare-section { 7897 margin: 1rem 0; 7898 display: block; 7899 } 7900 7901 .pd-compare-btn { 7902 width: 100%; 7903 font-size: 0.85rem; 7904 padding: 0.65rem 1rem; 7905 border-radius: 10px; 7906 justify-content: center; 7907 min-height: 44px;
7908 } 7909 7910 .pd-compare-btn .compare-icon { 7911 width: 16px; 7912 height: 16px; 7913 } 7914 7915 .pd-compare-info { 7916 margin-top: 0.5rem; 7917 display: block; 7918 text-align: center; 7919 } 7920 7921 .compare-note { 7922 font-size: 0.7rem; 7923 display: block; 7924 } 7925} 7926 7927/* Responsive Design */ 7928@media (max-width: 768px) { 7929 .compare-sidebar { 7930 width: 100%; 7931 right: -100%; 7932 /* Fix for mobile viewport height changes */ 7933 height: 100vh; 7934 height: 100dvh; /* Dynamic viewport height for modern browsers */ 7935 } 7936 7937 .compare-actions { 7938 /* Ensure actions are always visible */ 7939 position: sticky; 7940 bottom: 0; 7941 background: white; 7942 border-top: 2px solid #e2e8f0; 7943 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); 7944 z-index: 10; 7945 } 7946 7947 .compare-actions .btn { 7948 /* Larger touch targets for mobile */ 7949 min-height: 48px; 7950 font-size: 0.9rem; 7951 font-weight: 700; 7952 } 7953 7954 .modal-container { 7955 width: 95%; 7956 max-height: 95vh; 7957 max-height: 95dvh; /* Dynamic viewport height */ 7958 } 7959 7960 .modal-header, 7961 .modal-content, 7962 .modal-footer { 7963 padding: 1rem; 7964 } 7965 7966 .recommendations-grid { 7967 grid-template-columns: 1fr; 7968 } 7969 7970 .category-products { 7971 grid-template-columns: 1fr; 7972 } 7973 7974 .modal-footer { 7975 flex-direction: column; 7976 gap: 0.75rem; 7977 } 7978 7979 .modal-footer .btn { 7980 width: 100%; 7981 } 7982 7983 /* Ensure compare button is visible on mobile */ 7984 .pd-compare-section { 7985 display: block !important; 7986 visibility: visible !important; 7987 opacity: 1 !important; 7988 margin: 1rem 0; 7989 padding: 0; 7990 position: relative !important; 7991 z-index: 1 !important; 7992 max-width: 100%; 7993 overflow: hidden; 7994 } 7995 7996 .pd-compare-btn { 7997 display: flex !important; 7998 visibility: visible !important; 7999 opacity: 1 !important; 8000 width: 100%; 8001 padding: 0.75rem 1rem; 8002 font-size: 0.9rem; 8003 min-height: 48px; /* Minimum touch target for mobile */ 8004 border: 2px solid #e2e8f0 !important; 8005 background: #fff !important; 8006 position: relative !important; 8007 z-index: 2 !important; 8008 box-sizing: border-box; 8009 white-space: normal !important; /* Allow text wrapping on mobile */ 8010 flex-wrap: wrap; 8011 gap: 0.5rem; 8012 } 8013 8014 .pd-compare-btn .compare-text { 8015 display: inline !important; 8016 visibility: visible !important; 8017 font-weight: 600; 8018 color: #475569 !important; 8019 } 8020 8021 .pd-compare-btn .compare-remove { 8022 display: none !important; /* Hidden by default */ 8023 } 8024 8025 .pd-compare-btn.added .compare-remove { 8026 display: flex !important; /* Only show when added */ 8027 flex-wrap: wrap; 8028 align-items: center; 8029 gap: 0.4rem; 8030 white-space: normal !important; 8031 word-break: break-word; 8032 } 8033 8034 .pd-compare-btn .compare-icon { 8035 display: inline-block !important; 8036 width: 18px; 8037 height: 18px; 8038 flex-shrink: 0; 8039 } 8040 8041 .pd-compare-info { 8042 margin-top: 0.5rem; 8043 } 8044 8045 .compare-note { 8046 font-size: 0.85rem !important; 8047 display: block !important; 8048 visibility: visible !important; 8049 color: #64748b !important; 8050 } 8051} 8052 8053/* iOS Safari specific fixes */ 8054@media (max-width: 480px) { 8055 #compareBtn { 8056 width:46px; 8057 height:46px; 8058 } 8059 8060 .pd-compare-section { 8061 -webkit-transform: translateZ(0); 8062 transform: translateZ(0); 8063 } 8064 8065 .pd-compare-btn { 8066 -webkit-appearance: none !important; 8067 appearance: none !important; 8068 -webkit-tap-highlight-color: transparent; 8069 touch-action: manipulation; 8070 } 8071 8072 /* Fix sidebar height for iOS address bar */ 8073 .compare-sidebar { 8074 /* Use fixed positioning that accounts for iOS address bar */ 8075 height: 100vh; 8076 height: -webkit-fill-available; /* iOS Safari compatibility */ 8077 min-height: 100vh; 8078 min-height: -webkit-fill-available; 8079 } 8080 8081 /* Ensure compare actions stay at bottom */ 8082 .compare-actions { 8083 position: sticky; 8084 bottom: 0; 8085 background: white; 8086 border-top: 2px solid #e2e8f0; 8087 box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); 8088 padding: 1.25rem 1rem; 8089 z-index: 20; 8090 } 8091 8092 .compare-actions .btn { 8093 min-height: 52px; /* Larger for easier tapping */ 8094 font-size: 1rem; 8095 font-weight: 700; 8096 border-radius: 8px; 8097 } 8098 8099 /* Adjust content padding to account for sticky actions */ 8100 .compare-content { 8101 padding-bottom: 6rem; /* Extra space for sticky actions */ 8102 } 8103} 8104 8105/* High-resolution mobile devices (iPhone 12 Pro, etc.) */ 8106@media (-webkit-min-device-pixel-ratio: 2) and (max-width: 430px) { 8107 .pd-compare-section { 8108 display: block !important; 8109 } 8110 8111 .pd-compare-btn { 8112 display: flex !important; 8113 min-height: 44px;
8114 font-size: 16px; /* Prevent zoom on iOS */ 8115 } 8116 8117 /* Enhanced fixes for iPhone 12 Pro and similar devices */ 8118 .compare-sidebar { 8119 /* Ensure full height coverage */ 8120 height: 100vh; 8121 height: 100dvh; /* Dynamic viewport height */ 8122 height: -webkit-fill-available; /* iOS fallback */ 8123 max-height: 100vh; 8124 max-height: 100dvh; 8125 max-height: -webkit-fill-available; 8126 overflow: hidden; /* Prevent scroll issues */ 8127 } 8128 8129 .compare-content { 8130 /* Flexible content area */ 8131 flex: 1; 8132 overflow-y: auto; 8133 -webkit-overflow-scrolling: touch; 8134 padding: 1rem 1rem 8rem 1rem; /* Extra bottom padding */ 8135 } 8136 8137 .compare-actions { 8138 /* Force actions to stay visible */ 8139 position: fixed; 8140 bottom: 0; 8141 left: 0; 8142 right: 0; 8143 width: 100%; 8144 background: white; 8145 border-top: 2px solid #e2e8f0; 8146 box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); 8147 padding: 1rem; 8148 z-index: 9999; 8149 display: flex !important; 8150 gap: 0.75rem; 8151 } 8152 8153 .compare-actions .btn { 8154 flex: 1; 8155 min-height: 30px; /* Extra large for high-DPI screens */ 8156 font-size: .8rem; 8157 font-weight: 600; 8158 border-radius: 12px; 8159 display: flex !important; 8160 align-items: center; 8161 justify-content: center; 8162 gap: 0.5rem; 8163 } 8164 8165 /* Ensure AI analyze button is always visible and prominent */ 8166 #compareAnalyze { 8167 background: #EAB308 !important; 8168 border-color: #EAB308 !important; 8169 color: white !important; 8170 box-shadow: 0 4px 12px rgba(234, 179, 8, 0.3); 8171 } 8172 8173 #compareAnalyze:not(:disabled) { 8174 background: #CA8A04 !important; 8175 border-color: #CA8A04 !important; 8176 transform: scale(1.02); 8177 } 8178 8179 #compareAnalyze:disabled { 8180 opacity: 0.6 !important; 8181 background: #9ca3af !important; 8182 border-color: #9ca3af !important; 8183 } 8184} 8185 8186/* AI Disclaimer Styles */ 8187.ai-disclaimer { 8188 display: flex; 8189 align-items: flex-start; 8190 gap: 0.75rem; 8191 padding: 1rem; 8192 background: #FEF3C7; 8193 border: 1px solid #EAB308; 8194 border-radius: 8px; 8195 margin: 0; 8196} 8197 8198.disclaimer-icon { 8199 font-size: 1.25rem; 8200 flex-shrink: 0; 8201 margin-top: 0.125rem; 8202} 8203 8204.disclaimer-text { 8205 font-size: 0.85rem; 8206 line-height: 1.4; 8207 color: #8B6914; 8208} 8209 8210.disclaimer-text strong { 8211 color: #78350F; 8212 font-weight: 600; 8213} 8214 8215/* Compare Disclaimer in Sidebar */ 8216.compare-disclaimer { 8217 margin-top: 1rem; 8218 padding: 1rem; 8219 background: #FFFBEB; 8220 border: 1px solid #EAB308; 8221 border-radius: 8px; 8222} 8223 8224.compare-disclaimer h6 { 8225 display: flex; 8226 align-items: center; 8227 gap: 0.5rem; 8228 font-size: 0.875rem; 8229 font-weight: 600; 8230 color: #0c4a6e; 8231 margin-bottom: 0.5rem; 8232} 8233 8234.compare-disclaimer p { 8235 font-size: 0.8rem; 8236 line-height: 1.4; 8237 color: #075985; 8238 margin-bottom: 0.5rem; 8239} 8240 8241.compare-disclaimer p:last-child { 8242 margin-bottom: 0; 8243} 8244 8245.compare-disclaimer ul { 8246 list-style: none; 8247 padding: 0; 8248 margin: 0.5rem 0 0 0; 8249} 8250 8251.compare-disclaimer li { 8252 display: flex; 8253 align-items: flex-start; 8254 gap: 0.5rem; 8255 margin-bottom: 0.25rem; 8256 font-size: 0.75rem; 8257 line-height: 1.3; 8258 color: #075985; 8259} 8260 8261.compare-disclaimer li:before { 8262 content: "â¢"; 8263 color: #EAB308; 8264 font-weight: bold; 8265 margin-top: 0.1rem; 8266 flex-shrink: 0; 8267} 8268 8269/* Mobile Browser Address Bar Fixes */ 8270@supports (height: 100dvh) { 8271 /* Modern browsers with dynamic viewport support */ 8272 @media (max-width: 768px) { 8273 .compare-sidebar { 8274 height: 100dvh !important; 8275 max-height: 100dvh !important; 8276 } 8277 8278 .modal-container { 8279 max-height: 95dvh !important; 8280 } 8281 } 8282} 8283 8284@supports (height: -webkit-fill-available) { 8285 /* iOS Safari specific support */ 8286 @media (max-width: 768px) { 8287 .compare-sidebar { 8288 height: -webkit-fill-available !important; 8289 max-height: -webkit-fill-available !important; 8290 } 8291 } 8292} 8293 8294/* Fallback for older browsers */ 8295@media (max-width: 768px) and (orientation: portrait) { 8296 .compare-sidebar { 8297 /* Fixed height that works across different mobile browsers */ 8298 min-height: 600px; 8299 height: calc(100vh - env(keyboard-inset-height, 0px)); 8300 height: calc(100vh - env(safe-area-inset-bottom, 0px)); 8301 } 8302 8303 /* Ensure actions are always accessible */ 8304 .compare-actions { 8305 position: sticky !important; 8306 bottom: env(safe-area-inset-bottom, 0px) !important; 8307 margin-bottom: 0 !important; 8308 padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)) !important; 8309 } 8310} 8311 8312/* PWA and fullscreen app support */ 8313@media (display-mode: standalone) { 8314 .compare-sidebar { 8315 height: 100vh !important; 8316 height: 100dvh !important; 8317 } 8318} 8319 8320/* Utilities */
8321.btn-sm { 8322 padding: 0.5rem 0.75rem; 8323 font-size: 0.8rem; 8324} 8325 8326[hidden] { 8327 display: none !important; 8328} 8329 8330/* ===== UX ENHANCEMENTS ===== */ 8331 8332/* Compare Button Highlight Animation */ 8333.pulse-highlight { 8334 animation: pulse-glow 2s ease-in-out 3; 8335 box-shadow: 0 0 0 0 rgba(234, 179, 8, 0.7); 8336} 8337 8338@keyframes pulse-glow { 8339 0% { 8340 box-shadow: 0 0 0 0 rgba(234, 179, 8, 0.7); 8341 } 8342 50% { 8343 box-shadow: 0 0 0 10px rgba(234, 179, 8, 0); 8344 } 8345 100% { 8346 box-shadow: 0 0 0 0 rgba(234, 179, 8, 0); 8347 } 8348} 8349 8350/* Flying Product Animation */ 8351.flying-product { 8352 position: fixed; 8353 z-index: 9999; 8354 pointer-events: none; 8355} 8356 8357.flying-product img { 8358 border-radius: 8px; 8359 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 8360} 8361 8362/* Sticky Compare Bar - REMOVED (using only fly-to-navbar animation) */ 8363 8364/* Inline Compare Preview - REMOVED (keeping it simple and clean) */ 8365</style> 8366 <style data-inlined="product-card-actions.css"> 8367/** 8368 * Product Card Quick Actions - 3 Dot Menu + Bottom Sheet 8369 * Tokopedia-inspired design 8370 */ 8371 8372/* ===== 3 Dot Button on Product Card ===== */ 8373.prod-card { 8374 position: relative; 8375} 8376 8377.prod-card-actions-btn { 8378 position: absolute; 8379 top: 8px; 8380 right: 8px; 8381 width: 32px; 8382 height: 32px; 8383 border-radius: 50%; 8384 background: rgba(255, 255, 255, 0.95); 8385 border: 1px solid rgba(0, 0, 0, 0.08); 8386 display: inline-flex;
8387 align-items: center; 8388 justify-content: center; 8389 cursor: pointer; 8390 z-index: 3; 8391 transition: all 0.2s ease; 8392 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 8393 -webkit-tap-highlight-color: transparent; 8394} 8395 8396.prod-card-actions-btn:hover { 8397 background: #fff; 8398 border-color: rgba(0, 0, 0, 0.15); 8399 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 8400 transform: scale(1.05); 8401} 8402 8403.prod-card-actions-btn:active { 8404 transform: scale(0.95); 8405} 8406 8407.prod-card-actions-btn svg { 8408 width: 20px; 8409 height: 20px; 8410 color: #64748b; 8411 display: block; 8412} 8413 8414/* Desktop: Show on hover */ 8415@media (min-width: 641px) { 8416 .prod-card-actions-btn { 8417 opacity: 0; 8418 pointer-events: none; 8419 } 8420 8421 .prod-card:hover .prod-card-actions-btn { 8422 opacity: 1; 8423 pointer-events: auto; 8424 } 8425} 8426 8427/* Mobile: Always visible but smaller */ 8428@media (max-width: 640px) { 8429 .prod-card-actions-btn { 8430 width: 28px; 8431 height: 28px; 8432 top: 6px; 8433 right: 6px; 8434 opacity: 95%; 8435 } 8436 8437 .prod-card-actions-btn svg { 8438 width: 18px; 8439 height: 18px; 8440 } 8441} 8442 8443/* ===== Bottom Sheet Overlay ===== */ 8444.pca-overlay { 8445 position: fixed; 8446 inset: 0; 8447 background: rgba(15, 23, 42, 0.6); 8448 backdrop-filter: blur(4px); 8449 z-index: 99998; 8450 opacity: 0; 8451 visibility: hidden; 8452 transition: all 0.3s ease; 8453} 8454 8455.pca-overlay.active { 8456 opacity: 1; 8457 visibility: visible; 8458} 8459 8460/* ===== Bottom Sheet Container ===== */ 8461.pca-bottom-sheet { 8462 position: fixed; 8463 bottom: 0; 8464 left: 0; 8465 right: 0; 8466 background: #fff; 8467 border-radius: 20px 20px 0 0; 8468 box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15); 8469 z-index: 99999; 8470 max-width: 600px; 8471 margin: 0 auto; 8472 transform: translateY(100%); 8473 transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 8474} 8475 8476.pca-bottom-sheet.active { 8477 transform: translateY(0); 8478} 8479 8480/* Drag Handle */ 8481.pca-sheet-handle { 8482 width: 40px; 8483 height: 4px; 8484 background: #cbd5e1; 8485 border-radius: 999px; 8486 margin: 12px auto 8px; 8487} 8488 8489/* Product Preview */ 8490.pca-sheet-product { 8491 display: flex; 8492 gap: 12px; 8493 padding: 12px 20px; 8494 border-bottom: 1px solid #f1f5f9; 8495} 8496 8497.pca-product-image { 8498 width: 60px; 8499 height: 60px; 8500 border-radius: 8px; 8501 object-fit: cover; 8502 flex-shrink: 0; 8503 border: 1px solid #e2e8f0; 8504} 8505 8506.pca-product-info { 8507 flex: 1; 8508 min-width: 0; 8509} 8510 8511.pca-product-name { 8512 font-size: 14px; 8513 font-weight: 600; 8514 color: #0f172a; 8515 margin: 0 0 4px 0; 8516 display: -webkit-box; 8517 -webkit-line-clamp: 2; 8518 -webkit-box-orient: vertical; 8519 overflow: hidden; 8520 line-height: 1.3; 8521} 8522 8523.pca-product-price { 8524 font-size: 15px; 8525 font-weight: 700; 8526 color: #EAB308; 8527 margin: 0; 8528} 8529 8530.pca-product-stock { 8531 font-size: 12px; 8532 color: #64748b; 8533 margin: 2px 0 0 0; 8534} 8535 8536.pca-product-stock.in { 8537 color: #16a34a; 8538} 8539 8540.pca-product-stock.low { 8541 color: #CA8A04; 8542} 8543 8544.pca-product-stock.out { 8545 color: #dc2626; 8546} 8547 8548/* Action Menu */ 8549.pca-sheet-actions { 8550 padding: 8px 0 20px; 8551} 8552 8553.pca-action-item { 8554 display: flex;
8555 align-items: center; 8556 gap: 16px; 8557 padding: 16px 20px; 8558 background: transparent; 8559 border: 0; 8560 width: 100%; 8561 cursor: pointer; 8562 transition: background-color 0.15s ease; 8563 -webkit-tap-highlight-color: transparent; 8564} 8565 8566.pca-action-item:hover { 8567 background: #f8fafc; 8568} 8569 8570.pca-action-item:active { 8571 background: #f1f5f9; 8572} 8573 8574.pca-action-item:disabled { 8575 opacity: 0.4; 8576 cursor: not-allowed; 8577} 8578 8579.pca-action-item:disabled:hover { 8580 background: transparent; 8581} 8582 8583.pca-action-icon { 8584 width: 24px; 8585 height: 24px; 8586 color: #475569; 8587 flex-shrink: 0; 8588} 8589 8590.pca-action-content { 8591 flex: 1; 8592 text-align: left; 8593} 8594 8595.pca-action-label { 8596 font-size: 15px; 8597 font-weight: 600; 8598 color: #0f172a; 8599 margin: 0 0 2px 0; 8600} 8601 8602.pca-action-desc { 8603 font-size: 12px; 8604 color: #64748b; 8605 margin: 0; 8606} 8607 8608.pca-action-badge { 8609 display: inline-flex; 8610 align-items: center; 8611 justify-content: center; 8612 min-width: 20px; 8613 height: 20px; 8614 padding: 0 6px; 8615 background: #EAB308; 8616 color: #fff; 8617 font-size: 11px; 8618 font-weight: 700; 8619 border-radius: 999px; 8620 margin-left: auto; 8621} 8622 8623.pca-action-badge.compare { 8624 background: #CA8A04; 8625} 8626 8627/* Action Item Colors */ 8628.pca-action-item.cart .pca-action-icon { 8629 color: #EAB308; 8630} 8631 8632.pca-action-item.compare .pca-action-icon { 8633 color: #CA8A04; 8634} 8635 8636.pca-action-item.compare.active { 8637 background: rgba(234, 179, 8, 0.05); 8638} 8639 8640.pca-action-item.compare.active .pca-action-label { 8641 color: #CA8A04; 8642} 8643 8644.pca-action-item.compare.active .pca-action-icon { 8645 color: #CA8A04; 8646} 8647 8648.pca-action-item.share .pca-action-icon { 8649 color: #64748b; 8650} 8651 8652/* Loading State */ 8653.pca-action-item.loading { 8654 pointer-events: none; 8655} 8656 8657.pca-action-item.loading .pca-action-icon { 8658 animation: spin 1s linear infinite; 8659} 8660 8661@keyframes spin { 8662 from { transform: rotate(0deg); } 8663 to { transform: rotate(360deg); } 8664} 8665 8666/* Success State Animation */ 8667.pca-action-item.success .pca-action-icon { 8668 animation: checkmark 0.5s ease; 8669} 8670 8671@keyframes checkmark { 8672 0% { transform: scale(0); } 8673 50% { transform: scale(1.2); } 8674 100% { transform: scale(1); } 8675} 8676 8677/* ===== Safe Area for Mobile Devices ===== */ 8678@supports (padding-bottom: env(safe-area-inset-bottom)) { 8679 .pca-sheet-actions { 8680 padding-bottom: calc(20px + env(safe-area-inset-bottom)); 8681 } 8682} 8683 8684/* ===== Responsive Tweaks ===== */ 8685@media (max-width: 640px) { 8686 .pca-bottom-sheet { 8687 border-radius: 16px 16px 0 0; 8688 } 8689 8690 .pca-sheet-product { 8691 padding: 10px 16px; 8692 } 8693 8694 .pca-product-image { 8695 width: 50px; 8696 height: 50px; 8697 } 8698 8699 .pca-product-name { 8700 font-size: 13px; 8701 } 8702 8703 .pca-product-price { 8704 font-size: 14px; 8705 } 8706 8707 .pca-action-item { 8708 padding: 14px 16px; 8709 gap: 12px; 8710 } 8711 8712 .pca-action-icon { 8713 width: 22px; 8714 height: 22px; 8715 } 8716 8717 .pca-action-label { 8718 font-size: 14px; 8719 } 8720} 8721 8722/* ===== Animation for Flying Product ===== */ 8723.flying-product { 8724 position: fixed; 8725 pointer-events: none; 8726 z-index: 99999; 8727 animation: fadeOut 0.8s ease forwards; 8728} 8729 8730@keyframes fadeOut { 8731 to { 8732 opacity: 0; 8733 transform: scale(0.5); 8734 } 8735} 8736 8737/* ===== Desktop Dropdown (Alternative) ===== */ 8738@media (min-width: 1024px) { 8739 /* Optional: Could add desktop dropdown menu here */ 8740 /* For now, using bottom sheet for consistency */ 8741} 8742 8743/* ===== Print Styles ===== */ 8744@media print { 8745 .prod-card-actions-btn, 8746 .pca-overlay, 8747 .pca-bottom-sheet { 8748 display: none !important; 8749 } 8750} 8751 8752</style> 8753 <style data-inlined="modal.css"> 8754/* Modern Modal Dialog for Cart Operations */ 8755.modal-overlay { 8756 position: fixed; 8757 top: 0; 8758 left: 0; 8759 right: 0; 8760 bottom: 0; 8761 background: rgba(0, 0, 0, 0.5); 8762 backdrop-filter: blur(4px); 8763 display: flex; 8764 align-items: center; 8765 justify-content: center; 8766 z-index: 9999; 8767 opacity: 0; 8768 visibility: hidden; 8769 transition: all 0.2s ease; 8770} 8771 8772.modal-overlay.show { 8773 opacity: 1;
8774 visibility: visible; 8775} 8776 8777.modal-dialog { 8778 background: white; 8779 border-radius: 16px; 8780 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); 8781 max-width: 400px; 8782 width: 90%; 8783 max-height: 90vh; 8784 overflow-y: auto; 8785 transform: scale(0.95); 8786 transition: transform 0.2s ease; 8787} 8788 8789.modal-overlay.show .modal-dialog { 8790 transform: scale(1); 8791} 8792 8793.modal-header { 8794 padding: 24px 24px 16px; 8795 border-bottom: 1px solid #f1f5f9; 8796} 8797 8798.modal-title { 8799 font-size: 20px; 8800 font-weight: 700; 8801 color: #0f172a; 8802 margin: 0; 8803 display: flex; 8804 align-items: center; 8805 gap: 12px; 8806} 8807 8808.modal-body { 8809 padding: 16px 24px 24px; 8810} 8811 8812.modal-message { 8813 font-size: 16px; 8814 line-height: 1.5; 8815 color: #475569; 8816 margin: 0; 8817} 8818 8819.modal-actions { 8820 display: flex; 8821 gap: 12px; 8822 margin-top: 24px; 8823} 8824 8825.modal-btn { 8826 flex: 1; 8827 padding: 12px 16px; 8828 border: none; 8829 border-radius: 8px; 8830 font-size: 14px; 8831 font-weight: 600; 8832 cursor: pointer; 8833 transition: all 0.2s ease; 8834 text-align: center; 8835} 8836 8837.modal-btn-secondary { 8838 background: #f8fafc; 8839 color: #64748b; 8840 border: 1px solid #e2e8f0; 8841} 8842 8843.modal-btn-secondary:hover { 8844 background: #f1f5f9; 8845 border-color: #cbd5e1; 8846} 8847 8848.modal-btn-danger { 8849 background: #ef4444; 8850 color: white; 8851} 8852 8853.modal-btn-danger:hover { 8854 background: #dc2626; 8855} 8856 8857.modal-btn-primary { 8858 background: #EAB308; 8859 color: white; 8860} 8861 8862.modal-btn-primary:hover { 8863 background: #CA8A04; 8864} 8865 8866.modal-icon { 8867 font-size: 24px; 8868 width: 32px; 8869 height: 32px; 8870 display: flex; 8871 align-items: center; 8872 justify-content: center; 8873 border-radius: 8px; 8874} 8875 8876.modal-icon.danger { 8877 background: #fef2f2; 8878 color: #ef4444; 8879} 8880 8881.modal-icon.warning { 8882 background: #FFFBEB; 8883 color: #CA8A04; 8884} 8885 8886.modal-icon.success { 8887 background: #f0fdf4; 8888 color: #22c55e; 8889} 8890 8891.modal-icon.info { 8892 background: #FFFBEB; 8893 color: #EAB308; 8894} 8895 8896/* Mobile responsive */ 8897@media (max-width: 640px) { 8898 .modal-dialog { 8899 margin: 16px; 8900 max-width: none; 8901 width: auto; 8902 } 8903 8904 .modal-header, 8905 .modal-body { 8906 padding-left: 20px; 8907 padding-right: 20px; 8908 } 8909 8910 .modal-actions { 8911 flex-direction: column; 8912 } 8913 8914 .modal-btn { 8915 flex: none; 8916 } 8917} 8918</style> 8919 8920 <style data-inlined="reviews-points.css"> 8921/** 8922 * Reviews & Points System - Styles 8923 * Product reviews, rating display, and point redemption UI 8924 */ 8925 8926/* =================================== 8927 PRODUCT REVIEWS 8928 =================================== */ 8929 8930.product-tabs { 8931 display: flex; 8932 border-bottom: 2px solid #e5e7eb; 8933 margin: 24px 0 16px; 8934 gap: 8px; 8935} 8936 8937.tab-btn { 8938 padding: 12px 20px; 8939 border: none; 8940 background: none; 8941 cursor: pointer; 8942 font-size: 15px; 8943 font-weight: 500; 8944 color: #6b7280; 8945 border-bottom: 3px solid transparent; 8946 transition: all 0.2s ease; 8947 position: relative; 8948 top: 2px; 8949} 8950 8951.tab-btn:hover { 8952 color: #111827; 8953} 8954 8955.tab-btn.active { 8956 color: #CA8A04; 8957 border-bottom-color: #CA8A04; 8958} 8959 8960.tab-content { 8961 padding: 20px 0; 8962} 8963 8964/* Review Statistics */ 8965.review-stats { 8966 display: grid; 8967 grid-template-columns: 200px 1fr; 8968 gap: 32px; 8969 padding: 24px; 8970 background: #f9fafb; 8971 border-radius: 12px; 8972 margin-bottom: 24px; 8973} 8974 8975.rating-summary { 8976 text-align: center; 8977} 8978 8979.avg-rating { 8980 display: block; 8981 font-size: 48px; 8982 font-weight: 700; 8983 color: #111827; 8984 line-height: 1; 8985} 8986 8987.rating-summary .stars { 8988 font-size: 24px; 8989 color: #EAB308; 8990 margin: 8px 0; 8991} 8992 8993.total-reviews { 8994 display: block; 8995 font-size: 14px; 8996 color: #6b7280; 8997 margin-top: 4px; 8998} 8999 9000.rating-breakdown { 9001 display: flex; 9002 flex-direction: column; 9003 gap: 8px; 9004} 9005 9006.rating-bar { 9007 display: flex; 9008 align-items: center; 9009 gap: 12px; 9010} 9011 9012.rating-label { 9013 font-size: 14px; 9014 color: #374151; 9015 width: 32px; 9016 text-align: right; 9017} 9018 9019.bar-container { 9020 flex: 1; 9021 height: 8px; 9022 background: #e5e7eb; 9023 border-radius: 4px; 9024 overflow: hidden; 9025} 9026 9027.bar-fill { 9028 height: 100%; 9029 background: linear-gradient(90deg, #EAB308 0%, #CA8A04 100%); 9030 transition: width 0.3s ease; 9031} 9032 9033.rating-count { 9034 font-size: 14px; 9035 color: #6b7280; 9036 width: 40px; 9037 text-align: right; 9038} 9039 9040/* Review Filters */ 9041.review-filters { 9042 display: flex; 9043 justify-content: space-between; 9044 align-items: center; 9045 margin-bottom: 20px; 9046} 9047 9048.review-sort { 9049 padding: 8px 12px; 9050 border: 1px solid #d1d5db; 9051 border-radius: 8px; 9052 font-size: 14px; 9053 background: white; 9054} 9055 9056.write-review-btn { 9057 padding: 10px 20px; 9058 background: #CA8A04; 9059 color: white; 9060 border: none; 9061 border-radius: 8px; 9062 font-weight: 500;
9063 cursor: pointer; 9064 transition: background 0.2s; 9065} 9066 9067.write-review-btn:hover { 9068 background: #B45309; 9069} 9070 9071/* Review List */ 9072.reviews-list { 9073 display: flex; 9074 flex-direction: column; 9075 gap: 16px; 9076} 9077 9078.review-item { 9079 padding: 20px; 9080 background: white; 9081 border: 1px solid #e5e7eb; 9082 border-radius: 12px; 9083 transition: box-shadow 0.2s; 9084} 9085 9086.review-item:hover { 9087 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 9088} 9089 9090.review-header { 9091 display: flex; 9092 justify-content: space-between; 9093 align-items: flex-start; 9094 margin-bottom: 8px; 9095} 9096 9097.review-author { 9098 display: flex; 9099 align-items: center; 9100 gap: 8px; 9101} 9102 9103.review-author strong { 9104 font-size: 15px; 9105 color: #111827; 9106} 9107 9108.verified-badge { 9109 display: inline-flex; 9110 align-items: center; 9111 gap: 4px; 9112 font-size: 12px; 9113 color: #059669; 9114 background: #d1fae5; 9115 padding: 2px 8px; 9116 border-radius: 4px; 9117 font-weight: 500; 9118} 9119 9120.anonymous-badge { 9121 display: inline-flex; 9122 align-items: center; 9123 font-size: 12px; 9124 color: #6b7280; 9125 background: #f3f4f6; 9126 padding: 2px 6px; 9127 border-radius: 4px; 9128 margin-left: 4px; 9129 cursor: help; 9130} 9131 9132.review-rating .stars { 9133 font-size: 16px; 9134 color: #EAB308; 9135} 9136 9137.review-date { 9138 font-size: 13px; 9139 color: #9ca3af; 9140 margin-bottom: 12px; 9141} 9142 9143.review-text { 9144 font-size: 15px; 9145 line-height: 1.6; 9146 color: #374151; 9147 margin-bottom: 12px; 9148} 9149 9150.review-images { 9151 display: flex; 9152 gap: 8px; 9153 margin-bottom: 12px; 9154 flex-wrap: wrap; 9155} 9156 9157.review-img { 9158 width: 80px; 9159 height: 80px; 9160 object-fit: cover; 9161 border-radius: 8px; 9162 cursor: pointer; 9163 transition: transform 0.2s; 9164} 9165 9166.review-img:hover { 9167 transform: scale(1.05); 9168} 9169 9170.review-actions { 9171 display: flex; 9172 gap: 12px; 9173 padding-top: 12px; 9174 border-top: 1px solid #f3f4f6; 9175} 9176 9177.btn-helpful { 9178 display: flex; 9179 align-items: center; 9180 gap: 6px; 9181 padding: 6px 12px; 9182 background: #f9fafb; 9183 border: 1px solid #e5e7eb; 9184 border-radius: 6px; 9185 font-size: 13px; 9186 color: #6b7280; 9187 cursor: pointer; 9188 transition: all 0.2s; 9189} 9190 9191.btn-helpful:hover { 9192 background: #f3f4f6; 9193 color: #111827; 9194 border-color: #d1d5db; 9195} 9196 9197.btn-helpful.voted { 9198 background: #FEF3C7; 9199 color: #B45309; 9200 border-color: #FCD34D; 9201} 9202 9203.helpful-icon { 9204 font-size: 14px; 9205} 9206 9207.no-reviews { 9208 text-align: center; 9209 padding: 60px 20px; 9210 color: #9ca3af; 9211 font-size: 15px; 9212} 9213 9214.load-more-btn { 9215 display: block; 9216 margin: 24px auto 0; 9217 padding: 12px 32px; 9218 background: white; 9219 border: 1px solid #d1d5db; 9220 border-radius: 8px; 9221 font-size: 14px; 9222 font-weight: 500; 9223 color: #374151; 9224 cursor: pointer; 9225 transition: all 0.2s; 9226} 9227 9228.load-more-btn:hover { 9229 background: #f9fafb; 9230 border-color: #9ca3af; 9231} 9232 9233/* =================================== 9234 POINT BALANCE WIDGET (LEGACY - kept for backward compatibility) 9235 =================================== */ 9236 9237.point-balance-card { 9238 background: linear-gradient(135deg, #EAB308 0%, #8B6914 100%); 9239 color: white; 9240 padding: 24px; 9241 border-radius: 16px; 9242 margin-bottom: 24px; 9243 box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); 9244} 9245 9246.point-header { 9247 display: flex; 9248 justify-content: space-between; 9249 align-items: center; 9250 margin-bottom: 12px; 9251} 9252 9253.point-header h3 { 9254 font-size: 18px; 9255 font-weight: 600; 9256 margin: 0; 9257 display: flex; 9258 align-items: center; 9259 gap: 8px; 9260} 9261 9262.point-header .balance { 9263 font-size: 32px; 9264 font-weight: 700; 9265} 9266 9267.point-value { 9268 font-size: 14px; 9269 opacity: 0.9; 9270 margin: 8px 0 16px; 9271} 9272 9273.point-stats { 9274 display: flex; 9275 gap: 20px; 9276 font-size: 13px; 9277 opacity: 0.85; 9278 margin-bottom: 16px; 9279} 9280 9281.btn-view-history { 9282 display: inline-block; 9283 padding: 10px 20px; 9284 background: rgba(255, 255, 255, 0.2); 9285 color: white; 9286 text-decoration: none; 9287 border-radius: 8px; 9288 font-size: 14px; 9289 font-weight: 500; 9290 transition: background 0.2s; 9291} 9292 9293.btn-view-history:hover { 9294 background: rgba(255, 255, 255, 0.3); 9295} 9296 9297/* =================================== 9298 CHECKOUT POINT REDEMPTION 9299 =================================== */ 9300 9301.checkout-points { 9302 padding: 16px; 9303 background: #FEF3C7; 9304 border: 2px solid #EAB308; 9305 border-radius: 12px; 9306 margin-bottom: 16px; 9307} 9308 9309.points-header { 9310 display: flex; 9311 align-items: center; 9312 gap: 10px; 9313 margin-bottom: 12px; 9314} 9315 9316.points-header input[type="checkbox"] { 9317 width: 18px;
9318 height: 18px; 9319 cursor: pointer; 9320} 9321 9322.points-header label { 9323 font-size: 15px; 9324 color: #78350F; 9325 cursor: pointer; 9326 margin: 0; 9327} 9328 9329#points-slider { 9330 padding: 16px; 9331 background: white; 9332 border-radius: 8px; 9333 margin-top: 12px; 9334} 9335 9336#points-slider label { 9337 display: block; 9338 font-size: 14px; 9339 color: #374151; 9340 margin-bottom: 8px; 9341 font-weight: 500; 9342} 9343 9344#points-amount { 9345 width: 100%; 9346 margin-bottom: 8px; 9347} 9348 9349#points-display { 9350 display: flex; 9351 justify-content: space-between; 9352 font-size: 14px; 9353 color: #111827; 9354 margin-top: 8px; 9355} 9356 9357#points-display .discount-amount { 9358 color: #059669; 9359 font-weight: 600; 9360} 9361 9362.price-row.point-discount { 9363 color: #059669; 9364 font-weight: 500; 9365} 9366 9367/* =================================== 9368 REVIEWABLE ORDERS 9369 =================================== */ 9370 9371.reviewable-orders { 9372 background: white; 9373 border: 1px solid #e5e7eb; 9374 border-radius: 12px; 9375 padding: 20px; 9376 margin-bottom: 24px; 9377} 9378 9379.reviewable-orders h3 { 9380 font-size: 18px; 9381 margin-bottom: 16px; 9382 color: #111827; 9383} 9384 9385.order-card { 9386 display: flex; 9387 gap: 16px; 9388 padding: 16px; 9389 background: #f9fafb; 9390 border-radius: 8px; 9391 margin-bottom: 12px; 9392} 9393 9394.order-card:last-child { 9395 margin-bottom: 0; 9396} 9397 9398.order-card img { 9399 width: 80px; 9400 height: 80px; 9401 object-fit: cover; 9402 border-radius: 8px; 9403} 9404 9405.order-info { 9406 flex: 1; 9407} 9408 9409.order-info h4 { 9410 font-size: 15px; 9411 margin: 0 0 4px; 9412 color: #111827; 9413} 9414 9415.order-info p { 9416 font-size: 13px; 9417 color: #6b7280; 9418 margin: 0 0 12px; 9419} 9420 9421.btn-review { 9422 padding: 8px 16px; 9423 background: #059669; 9424 color: white; 9425 border: none; 9426 border-radius: 6px; 9427 font-size: 14px; 9428 font-weight: 500; 9429 cursor: pointer; 9430 transition: background 0.2s; 9431} 9432 9433.btn-review:hover { 9434 background: #047857; 9435} 9436 9437/* =================================== 9438 TOAST NOTIFICATIONS 9439 =================================== */ 9440 9441.review-toast { 9442 position: fixed; 9443 bottom: 24px; 9444 right: 24px; 9445 padding: 14px 20px; 9446 background: #111827; 9447 color: white; 9448 border-radius: 8px; 9449 font-size: 14px; 9450 line-height: 1.4; 9451 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 9452 transform: translateY(100px); 9453 opacity: 0; 9454 transition: all 0.3s ease; 9455 z-index: 10000; 9456 max-width: 400px; 9457 word-wrap: break-word; 9458} 9459 9460.review-toast.show { 9461 transform: translateY(0); 9462 opacity: 1; 9463} 9464 9465/* Mobile: Position above bottom-nav */ 9466@media (max-width: 768px) { 9467 .review-toast { 9468 bottom: 80px; /* Above bottom-nav (50px height + padding + safe margin) */ 9469 right: 16px; 9470 left: 16px; 9471 max-width: calc(100% - 32px); 9472 } 9473} 9474 9475.review-toast.toast-success { 9476 background: #059669; 9477} 9478 9479.review-toast.toast-error { 9480 background: #dc2626; 9481} 9482 9483.review-toast.toast-info { 9484 background: #CA8A04; 9485} 9486 9487/* =================================== 9488 RESPONSIVE 9489 =================================== */ 9490 9491@media (max-width: 768px) { 9492 .point-balance-card { 9493 border-radius: 0; 9494 } 9495 9496 .review-stats { 9497 grid-template-columns: 70px 1fr; 9498 gap: 20px; 9499 padding: 16px; 9500 } 9501 9502 .rating-summary { 9503 text-align: center; 9504 } 9505 9506 .rating-summary .avg-rating { 9507 font-size: 32px; 9508 } 9509 9510 .rating-summary .stars { 9511 font-size: 18px; 9512 margin: 4px 0; 9513 } 9514 9515 .total-reviews { 9516 font-size: 11px; 9517 } 9518 9519 /* Rating breakdown - more compact */ 9520 .rating-breakdown { 9521 gap: 2px; 9522 } 9523 9524 .rating-breakdown .rating-bar { 9525 gap: 4px; 9526 } 9527 9528 .rating-breakdown .rating-label { 9529 font-size: 11px; 9530 width: 16px; 9531 min-width: 16px; 9532 text-align: left; 9533 } 9534 9535 .rating-breakdown .bar-container { 9536 height: 6px; 9537 } 9538 9539 .rating-breakdown .rating-count { 9540 font-size: 11px; 9541 width: 16px; 9542 min-width: 16px; 9543 } 9544 9545 .review-header { 9546 flex-direction: column; 9547 gap: 8px; 9548 align-items: flex-start; 9549 } 9550 9551 .review-rating { 9552 order: -1; 9553 margin-bottom: 4px; 9554 } 9555 9556 .review-rating .stars { 9557 font-size: 14px; 9558 } 9559 9560 .review-filters { 9561 flex-direction: column; 9562 gap: 12px;
9563 align-items: stretch; 9564 } 9565 9566 .review-sort { 9567 width: 100%; 9568 } 9569 9570 .point-header .balance { 9571 font-size: 24px; 9572 } 9573 9574 .point-stats { 9575 flex-direction: column; 9576 gap: 8px; 9577 } 9578 9579 .order-card { 9580 flex-direction: column; 9581 } 9582 9583 .order-card img { 9584 width: 100%; 9585 height: 200px; 9586 } 9587} 9588 9589/* =================================== 9590 LOADING & ERROR STATES 9591 =================================== */ 9592 9593.review-loading { 9594 text-align: center; 9595 padding: 40px; 9596 color: #9ca3af; 9597 font-size: 14px; 9598} 9599 9600.review-error { 9601 text-align: center; 9602 padding: 40px; 9603 color: #dc2626; 9604 font-size: 14px; 9605 background: #fee2e2; 9606 border-radius: 8px; 9607} 9608 9609.review-empty { 9610 text-align: center; 9611 padding: 10px; 9612 color: #6b7280; 9613 font-size: 14px; 9614} 9615 9616/* =================================== 9617 REVIEWABLE ORDERS LIST 9618 =================================== */ 9619 9620.reviewable-orders-list { 9621 display: flex; 9622 flex-direction: column; 9623 gap: 16px; 9624} 9625 9626.reviewable-order-item { 9627 display: grid; 9628 grid-template-columns: 80px 1fr auto; 9629 gap: 16px; 9630 padding: 16px; 9631 background: #fff; 9632 border: 1px solid #e5e7eb; 9633 border-radius: 12px; 9634 align-items: center; 9635 transition: box-shadow 0.2s; 9636} 9637 9638.reviewable-order-item:hover { 9639 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 9640} 9641 9642.reviewable-order-item .roi-image { 9643 width: 80px; 9644 height: 80px; 9645 object-fit: cover; 9646 border-radius: 8px; 9647 border: 1px solid #e5e7eb; 9648} 9649 9650.reviewable-order-item .roi-info { 9651 display: flex; 9652 flex-direction: column; 9653 gap: 4px; 9654} 9655 9656.reviewable-order-item .roi-info h4 { 9657 margin: 0; 9658 font-size: 15px; 9659 font-weight: 600; 9660 color: #111827; 9661 line-height: 1.4; 9662} 9663 9664.reviewable-order-item .roi-meta { 9665 margin: 0; 9666 font-size: 13px; 9667 color: #6b7280; 9668} 9669 9670.reviewable-order-item .btn-write-review { 9671 white-space: nowrap; 9672 padding: 10px 20px; 9673 font-size: 14px; 9674} 9675 9676@media (max-width: 640px) { 9677 .reviewable-order-item { 9678 grid-template-columns: 60px 1fr; 9679 gap: 12px; 9680 } 9681 9682 .reviewable-order-item .roi-image { 9683 width: 60px; 9684 height: 60px; 9685 } 9686 9687 .reviewable-order-item .btn-write-review { 9688 grid-column: 1 / -1; 9689 width: 100%; 9690 } 9691} 9692 9693/* =================================== 9694 REVIEW SUBMISSION MODAL 9695 =================================== */ 9696 9697/* Modal Overlay Background */ 9698#review-modal-overlay { 9699 position: fixed; 9700 top: 0; 9701 left: 0; 9702 right: 0; 9703 bottom: 0; 9704 background: rgba(0, 0, 0, 0.6) !important; 9705 backdrop-filter: blur(8px); 9706 display: flex; 9707 align-items: center; 9708 justify-content: center; 9709 z-index: 99999 !important; 9710 padding: 20px; 9711} 9712 9713.review-modal { 9714 max-width: 600px !important; 9715 width: 90% !important; 9716 background: white; 9717 border-radius: 16px !important; 9718 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); 9719 max-height: 90vh; 9720 display: flex; 9721 flex-direction: column; 9722 overflow: hidden !important; 9723} 9724 9725#review-modal-overlay .modal-content, 9726.review-modal .modal-content { 9727 padding: 0 !important; 9728 flex: initial !important; 9729 overflow-y: visible !important; 9730} 9731 9732.review-modal .modal-header { 9733 display: flex; 9734 justify-content: space-between; 9735 align-items: center; 9736 padding: 20px 24px; 9737 border-bottom: 1px solid #e5e7eb; 9738} 9739 9740.review-modal .modal-title { 9741 margin: 0; 9742 font-size: 20px; 9743 font-weight: 700; 9744 color: #111827; 9745} 9746 9747.review-modal .modal-close { 9748 background: none; 9749 border: none; 9750 cursor: pointer; 9751 padding: 4px; 9752 color: #6b7280; 9753 transition: color 0.2s; 9754 display: flex; 9755 align-items: center; 9756 justify-content: center; 9757} 9758
9759.review-modal .modal-close:hover { 9760 color: #111827; 9761} 9762 9763.review-modal .modal-body { 9764 padding: 24px; 9765 max-height: calc(90vh - 140px); 9766 overflow-y: auto; 9767} 9768 9769/* Custom Scrollbar for Modal */ 9770.review-modal .modal-body::-webkit-scrollbar { 9771 width: 8px; 9772} 9773 9774.review-modal .modal-body::-webkit-scrollbar-track { 9775 background: #f1f5f9; 9776 border-radius: 4px; 9777} 9778 9779.review-modal .modal-body::-webkit-scrollbar-thumb { 9780 background: linear-gradient(180deg, #EAB308 0%, #CA8A04 100%); 9781 border-radius: 4px; 9782} 9783 9784.review-modal .modal-body::-webkit-scrollbar-thumb:hover { 9785 background: linear-gradient(180deg, #CA8A04 0%, #B45309 100%); 9786} 9787 9788/* Firefox Scrollbar */ 9789.review-modal .modal-body { 9790 scrollbar-width: thin; 9791 scrollbar-color: #EAB308 #f1f5f9; 9792} 9793 9794.review-modal .modal-footer { 9795 display: flex; 9796 gap: 12px; 9797 margin-top: 24px; 9798 padding-top: 20px; 9799 border-top: 1px solid #f1f5f9; 9800} 9801 9802.review-product-info { 9803 display: flex; 9804 gap: 16px; 9805 padding: 16px; 9806 background: #f9fafb; 9807 border-radius: 8px; 9808 margin-bottom: 24px; 9809} 9810 9811.review-product-thumb { 9812 width: 80px; 9813 height: 80px; 9814 object-fit: cover; 9815 border-radius: 6px; 9816 border: 1px solid #e5e7eb; 9817} 9818 9819.review-product-details h4 { 9820 margin: 0 0 8px 0; 9821 font-size: 16px; 9822 font-weight: 600; 9823 color: #111827; 9824} 9825 9826.review-product-details p { 9827 margin: 0; 9828 font-size: 13px; 9829 color: #6b7280; 9830} 9831 9832/* Star Rating Input */ 9833.star-rating-input { 9834 display: inline-flex; 9835 gap: 8px; 9836 margin-right: 12px; 9837 vertical-align: middle; 9838} 9839 9840.star-btn { 9841 background: none; 9842 border: none; 9843 font-size: 32px; 9844 color: #d1d5db; 9845 cursor: pointer; 9846 transition: all 0.2s; 9847 padding: 0; 9848 line-height: 1; 9849} 9850 9851.star-btn:hover, 9852.star-btn.active { 9853 color: #EAB308; 9854 transform: scale(1.1); 9855} 9856 9857.rating-label { 9858 display: inline-block; 9859 font-size: 15px; 9860 color: #6b7280; 9861 margin: 0; 9862 vertical-align: middle; 9863 min-width: 150px; 9864} 9865 9866/* Form Elements */ 9867.review-form .form-group { 9868 margin-bottom: 20px; 9869} 9870 9871.review-form .form-label { 9872 display: block; 9873 font-size: 14px; 9874 font-weight: 600; 9875 color: #374151; 9876 margin-bottom: 8px; 9877} 9878 9879.review-form .form-control { 9880 width: 100%; 9881 padding: 12px; 9882 border: 1px solid #d1d5db; 9883 border-radius: 8px; 9884 font-size: 14px; 9885 font-family: inherit; 9886 transition: border-color 0.2s; 9887} 9888 9889.review-form .form-control:focus { 9890 outline: none; 9891 border-color: #CA8A04; 9892 box-shadow: 0 0 0 3px rgba(202, 138, 4, 0.1); 9893} 9894 9895.review-form .form-hint { 9896 font-size: 13px; 9897 color: #6b7280; 9898 margin-top: 8px; 9899} 9900 9901/* Image Upload */ 9902.btn-upload-trigger { 9903 display: inline-flex; 9904 align-items: center; 9905 gap: 8px; 9906 padding: 10px 16px; 9907 background: #f9fafb; 9908 border: 2px dashed #d1d5db; 9909 border-radius: 8px; 9910 color: #374151; 9911 cursor: pointer; 9912 font-size: 14px; 9913 font-weight: 500; 9914 transition: all 0.2s; 9915} 9916 9917.btn-upload-trigger:hover { 9918 background: #f3f4f6; 9919 border-color: #CA8A04; 9920 color: #CA8A04; 9921} 9922 9923.image-preview-grid { 9924 display: grid; 9925 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 9926 gap: 12px; 9927 margin-top: 16px; 9928} 9929 9930.image-preview-item { 9931 position: relative; 9932 aspect-ratio: 1; 9933 border-radius: 8px; 9934 overflow: hidden; 9935 border: 1px solid #e5e7eb; 9936} 9937 9938.image-preview-item img { 9939 width: 100%; 9940 height: 100%; 9941 object-fit: cover; 9942} 9943 9944.remove-image { 9945 position: absolute; 9946 top: 4px; 9947 right: 4px; 9948 background: rgba(0, 0, 0, 0.7); 9949 border: none; 9950 color: white; 9951 width: 24px; 9952 height: 24px; 9953 border-radius: 50%; 9954 cursor: pointer; 9955 display: flex; 9956 align-items: center; 9957 justify-content: center; 9958 transition: background 0.2s; 9959} 9960 9961.remove-image:hover { 9962 background: rgba(0, 0, 0, 0.9); 9963} 9964 9965/* Point Reward Info */ 9966.point-reward-info { 9967 background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%); 9968 border: 2px solid #EAB308; 9969 border-radius: 12px; 9970 padding: 16px; 9971 margin: 20px 0; 9972} 9973 9974.pri-header { 9975 display: flex; 9976 align-items: center; 9977 gap: 8px; 9978 font-size: 16px; 9979 font-weight: 700;
9980 color: #8B6914; 9981 margin-bottom: 12px; 9982} 9983 9984.pri-list { 9985 list-style: none; 9986 padding: 0; 9987 margin: 0 0 12px 0; 9988} 9989 9990.pri-list li { 9991 padding: 6px 0; 9992 font-size: 14px; 9993 color: #78350F; 9994 transition: opacity 0.2s; 9995} 9996 9997.pri-total { 9998 padding-top: 12px; 9999 border-top: 2px solid rgba(234, 179, 8, 0.3); 10000 font-size: 16px; 10001 color: #8B6914; 10002 font-weight: 600; 10003} 10004 10005.form-error { 10006 background: #fee2e2; 10007 border: 1px solid #f87171; 10008 color: #991b1b; 10009 padding: 12px; 10010 border-radius: 8px; 10011 font-size: 14px; 10012 margin-top: 16px; 10013} 10014 10015/* Buttons */ 10016.review-form .btn { 10017 padding: 12px 24px; 10018 border: none; 10019 border-radius: 8px; 10020 font-size: 15px; 10021 font-weight: 600; 10022 cursor: pointer; 10023 transition: all 0.2s; 10024} 10025 10026.review-form .btn-primary { 10027 background: #CA8A04; 10028 color: white; 10029} 10030 10031.review-form .btn-primary:hover { 10032 background: #B45309; 10033} 10034 10035.review-form .btn-primary:disabled { 10036 background: #94a3b8; 10037 cursor: not-allowed; 10038} 10039 10040.review-form .btn-secondary { 10041 background: #f1f5f9; 10042 color: #64748b; 10043} 10044 10045.review-form .btn-secondary:hover { 10046 background: #e2e8f0; 10047 color: #475569; 10048} 10049 10050@media (max-width: 768px) { 10051 #review-modal-overlay { padding: 0; } 10052 .review-modal { max-width: 100%; width: 100% !important; } 10053} 10054 10055/* =================================== 10056 POINT SUMMARY GRID (Profile Page) 10057 =================================== */ 10058 10059.point-summary-grid { 10060 display: grid; 10061 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 10062 gap: 12px; 10063 margin-bottom: 32px; 10064} 10065 10066.point-summary-card { 10067 background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); 10068 border: 1px solid #e5e7eb; 10069 border-radius: 12px; 10070 padding: 16px 20px; 10071 display: flex; 10072 align-items: center; 10073 justify-content: space-between; 10074 gap: 12px; 10075 transition: all 0.2s; 10076} 10077 10078.point-summary-card:hover { 10079 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 10080 transform: translateY(-2px); 10081} 10082 10083.point-summary-card .psc-left { 10084 display: flex; 10085 flex-direction: column; 10086 gap: 2px; 10087} 10088 10089.point-summary-card .psc-icon { 10090 font-size: 24px; 10091 margin-bottom: 4px; 10092} 10093 10094.point-summary-card .psc-label { 10095 font-size: 13px; 10096 color: #6b7280; 10097 font-weight: 500; 10098 text-transform: uppercase; 10099 letter-spacing: 0.5px; 10100} 10101 10102.point-summary-card .psc-value { 10103 font-size: 26px; 10104 font-weight: 700; 10105 color: #111827; 10106 flex-shrink: 0; 10107} 10108 10109/* Point History Section */ 10110.point-history-section { 10111 margin-top: 32px; 10112} 10113 10114.point-history-section h3 { 10115 font-size: 18px; 10116 font-weight: 600; 10117 color: #111827; 10118 margin-bottom: 16px; 10119} 10120 10121.point-history-list { 10122 display: flex; 10123 flex-direction: column; 10124 gap: 12px; 10125} 10126 10127.point-history-item { 10128 display: grid; 10129 grid-template-columns: 40px 1fr auto; 10130 gap: 16px; 10131 padding: 16px; 10132 background: white; 10133 border: 1px solid #e5e7eb; 10134 border-radius: 10px; 10135 align-items: center; 10136 transition: all 0.2s; 10137} 10138 10139.point-history-item:hover { 10140 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); 10141} 10142 10143.point-history-item.credit { 10144 border-left: 3px solid #10b981; 10145} 10146 10147.point-history-item.debit { 10148 border-left: 3px solid #CA8A04; 10149} 10150 10151.point-history-item .phi-icon { 10152 width: 40px; 10153 height: 40px; 10154 display: flex; 10155 align-items: center; 10156 justify-content: center; 10157 font-size: 20px; 10158 border-radius: 50%; 10159 background: #f9fafb; 10160} 10161 10162.point-history-item.credit .phi-icon { 10163 background: #d1fae5; 10164} 10165 10166.point-history-item.debit .phi-icon { 10167 background: #FEF3C7; 10168} 10169
10170.point-history-item .phi-details { 10171 display: flex; 10172 flex-direction: column; 10173 gap: 4px; 10174} 10175 10176.point-history-item .phi-desc { 10177 font-size: 15px; 10178 font-weight: 500; 10179 color: #111827; 10180} 10181 10182.point-history-item .phi-date { 10183 font-size: 13px; 10184 color: #9ca3af; 10185} 10186 10187.point-history-item .phi-amount { 10188 font-size: 18px; 10189 font-weight: 700; 10190} 10191 10192.point-history-item .phi-amount.credit { 10193 color: #10b981; 10194} 10195 10196.point-history-item .phi-amount.debit { 10197 color: #CA8A04; 10198} 10199 10200@media (max-width: 640px) { 10201 .point-summary-grid { 10202 grid-template-columns: 1fr; 10203 } 10204 10205 .point-summary-card .psc-value { 10206 font-size: 22px; 10207 } 10208 10209 .point-history-item { 10210 grid-template-columns: 36px 1fr; 10211 gap: 12px; 10212 } 10213 10214 .point-history-item .phi-amount { 10215 grid-column: 2; 10216 text-align: right; 10217 margin-top: 8px; 10218 } 10219} 10220 10221/* ââ Pending points: compact widget indicator ââ */ 10222.pbc-pending { 10223 font-size: 12px; 10224 opacity: 0.85; 10225 margin-top: 4px; 10226 color: #FDE68A; 10227} 10228 10229/* ââ Pending points: summary card ââ */ 10230.point-summary-card.psc-pending-card { 10231 background: linear-gradient(135deg, #FFFBEB 0%, #FEF3C7 100%); 10232 border-color: #FBBF24; 10233} 10234 10235.point-summary-card.psc-pending-card .psc-value { 10236 color: #78350F; 10237} 10238 10239.point-summary-card .psc-sub { 10240 font-size: 11px; 10241 color: #9ca3af; 10242 margin-top: 4px; 10243} 10244 10245/* ââ Pending points: history item ââ */ 10246.point-history-item.phi-pending { 10247 opacity: 0.75; 10248 border-left-color: #FBBF24; 10249 background: #FFFBEB; 10250} 10251 10252.point-history-item.phi-pending .phi-icon { 10253 background: #FEF3C7; 10254} 10255 10256.phi-pending-badge { 10257 display: inline-block; 10258 font-size: 11px; 10259 font-weight: 600; 10260 padding: 2px 7px; 10261 border-radius: 10px; 10262 background: #FEF3C7; 10263 color: #8B6914; 10264 border: 1px solid #FBBF24; 10265 margin-left: 8px; 10266 vertical-align: middle; 10267} 10268 10269/* ââ Reversed points: history item ââ */ 10270.point-history-item.phi-reversed { 10271 opacity: 0.55; 10272 border-left-color: #d1d5db; 10273 background: #f9fafb; 10274} 10275 10276.point-history-item.phi-reversed .phi-icon { 10277 background: #f3f4f6; 10278} 10279 10280.point-history-item.phi-reversed .phi-amount { 10281 text-decoration: line-through; 10282 color: #9ca3af !important; 10283} 10284 10285.phi-reversed-badge { 10286 display: inline-block; 10287 font-size: 11px; 10288 font-weight: 600; 10289 padding: 2px 7px; 10290 border-radius: 10px; 10291 background: #fee2e2; 10292 color: #991b1b; 10293 border: 1px solid #fca5a5; 10294 margin-left: 8px; 10295 vertical-align: middle; 10296} 10297 10298</style> 10299 10300</head> 10301<body> 10302<header class="topbar"> 10303 <div class="container topbar-inner"> 10304 <div class="topbar-left"> 10305 EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap 10306 </div> 10307<div class="topbar-right"> 10308 <a href="https://norita.mx/">EDUTOTO</a> 10309 <a href="https://norita.mx/">EDUTOTO LOGIN</a> 10310 <a href="https://norita.mx/">ALTERNATIF EDUTOTO</a> 10311 <a href="https://norita.mx/">WEBSITE EDUTOTO</a> 10312</div> 10313 </div> 10314</header> 10315<nav class="navbar"> 10316 <nav class="navbar-logo" style="width: 100%;"> 10317 <div class="container nav-inner" style="justify-items: center; justify-content: center;"> 10318 <a href="https://norita.mx/" class="logo-mobile" title="EDUTOTO"> 10319 <img src="/img/logo-web.png" alt="EDUTOTO" height="40"> 10320 </a> 10321 </div> 10322</nav> 10323 <div class="container nav-inner"> 10324 <a href="https://norita.mx/" class="logo" title="EDUTOTO"> 10325 <img src="/img/logo-web.png" alt="EDUTOTO" height="40"> 10326 </a> 10327 10328 10329 <form class="search" method="get" action="#"> 10330 <input type="hidden" name="page" value="produk"> 10331 10332 <!-- Search Icon (Desktop) --> 10333 <button type="submit" class="search-icon-btn" id="searchBtn" aria-label="Cari"> 10334 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 10335 <circle cx="11" cy="11" r="8"/> 10336 <path d="m21 21-4.35-4.35"/> 10337 </svg> 10338 </button> 10339 10340 <input type="text" name="q" id="searchInput" placeholder="Cari Aja Di Google EDUTOTO Bre !" 10341 value="" autocomplete="off"> 10342 10343 <div class="search-suggest" id="searchSuggest" hidden></div> 10344 </form> 10345 10346 <div class="nav-actions"> 10347 <!--<button class="icon-btn" id="notifBtn" title="Notifikasi">ð</button>--> 10348 <button class="icon-btn cart-indicator" id="cartBtn" title="Keranjang"> 10349 ð <span id="cartCount" class="badge" data-role="cart-badge" aria-live="polite">0</span> 10350 </button> 10351 10352 <button class="icon-btn compare-indicator" id="compareBtn" title="Perbandingan"> 10353 âï¸ <span id="compareCount" class="badge" data-role="compare-badge" aria-live="polite">0</span> 10354 </button> 10355 10356 <!-- Dynamic login/logout button --> 10357 <button class="login-btn" id="loginBtn" aria-label="Masuk">
10358 <span class="login-text">Masuk</span> 10359 <span class="login-ico" aria-hidden="true">ð</span> 10360 </button> 10361 </div> 10362 </div> 10363 10364 <div class="container" style="display: flex; align-items: center;"> 10365 10366 10367<div class="mega-cats-wrapper"> 10368 <div class="mega-cats container" id="megaCatsScroll" style="padding-left: 5px !important;"> 10369 <a class="" href="https://norita.mx/"> 10370 EDUTOTO 10371 </a> 10372 <a class="" href="https://norita.mx/"> 10373 EDUTOTO LOGIN 10374 </a> 10375 <a class="" href="https://norita.mx/"> 10376 ALTERNATIF EDUTOTO 10377 </a> 10378 <a class="" href="https://norita.mx/"> 10379 EDUTOTO DAFTAR 10380 </a> 10381 <a class="" href="https://norita.mx/"> 10382 EDUTOTO SLOT 10383 </a> 10384 <a class="" href="https://norita.mx/"> 10385 WEBSITE EDUTOTO 10386 </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://norita.mx/'; 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> EDUTOTO menghadirkan sensasi relaksasi premium dengan akses nyaman dan fitur lengkap. Informasi di halaman ini membantu Anda memahami kenyamanan platform, alur penggunaan, serta pengalaman bermain yang lebih santai. 11222</div> 11223 </div> 11224 </div> 11225</div><style data-inlined="pd-lightbox.css"> 11226/* PlazaIT Product Lightbox (vanilla, no CDN) â v4 */ 11227.pd-lb { 11228 position: fixed; inset: 0; z-index: 99999; 11229 background: rgba(0,0,0,.95); 11230 display: none; 11231 color: #fff; 11232 /* Transition untuk swipe-down feedback */ 11233 transition: transform .18s ease, opacity .18s ease; 11234} 11235.pd-lb.show { display: block; } 11236 11237/* Canvas di bawah (z:1), Controls di atas (z:2) */ 11238.pd-lb-canvas { 11239 position: absolute; inset: 56px 0 56px 0; 11240 display: flex; align-items: center; justify-content: center; 11241 overflow: hidden; 11242 touch-action: none; 11243 z-index: 1; 11244} 11245 11246.pd-lb-img { 11247 max-width: 96vw; max-height: 86vh; 11248 will-change: transform; 11249 user-select: none; 11250 pointer-events: none; 11251 transform-origin: 50% 50%; 11252} 11253 11254.pd-lb-close, .pd-lb-prev, .pd-lb-next, .pd-lb-counter { 11255 position: absolute; 11256 z-index: 2; 11257} 11258 11259.pd-lb-close, .pd-lb-prev, .pd-lb-next { 11260 background: rgba(17,24,39,.7); 11261 color: #fff; border: 0; cursor: pointer; 11262 width: 40px; height: 40px; border-radius: 999px; 11263 display: inline-flex; align-items: center; justify-content: center; 11264 transition: background-color .15s ease, transform .05s ease; 11265 -webkit-tap-highlight-color: transparent; 11266} 11267.pd-lb-close:hover, .pd-lb-prev:hover, .pd-lb-next:hover { background: rgba(31,41,55,.85); } 11268.pd-lb-close:active, .pd-lb-prev:active, .pd-lb-next:active { transform: translateY(.5px); } 11269 11270.pd-lb-close { top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); } 11271.pd-lb-prev { top: 50%; left: 10px; transform: translateY(-50%); } 11272.pd-lb-next { top: 50%; right: 10px; transform: translateY(-50%); } 11273 11274.pd-lb-counter { 11275 top: max(16px, env(safe-area-inset-top, 16px)); left: 50%; 11276 transform: translateX(-50%); 11277 font-weight: 700; font-size: 13px; letter-spacing: .4px;
11278 background: rgba(17,24,39,.65); 11279 padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.1); 11280} 11281 11282@media (max-width: 640px){ 11283 .pd-lb-img { max-width: 98vw; max-height: 80vh; } 11284 .pd-lb-prev, .pd-lb-next { width: 36px; height: 36px; } 11285 .pd-lb-close { width: 38px; height: 38px; } 11286} 11287</style> 11288<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,#EAB308 0,#8B6914 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:#EAB308;cursor:pointer;transition:all .2s ease;white-space:nowrap}.pdv-min-copy:hover:not(:disabled),.pdv-min-more:hover{background:#EAB308;color:#fff;border-color:#EAB308}.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,#EAB308 0,#8B6914 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:#EAB308;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:#EAB308;flex-shrink:0;margin-top:2px}.vmd-item strong{color:#1e293b}.modal-actions{padding:20px}.modal-btn-primary{background:linear-gradient(135deg,#EAB308 0,#8B6914 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(201,162,39,.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(201,162,39,.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:#C9A227;margin-bottom:2px}
11288.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,#C9A227,#FFB84D);color:#fff;text-decoration:none;font-size:13px;font-weight:700;border-radius:8px;white-space:nowrap;transition:all .2s ease;box-shadow:0 2px 8px rgba(201,162,39,.25)}.pey-link:hover{transform:translateX(2px);box-shadow:0 4px 12px rgba(201,162,39,.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> 11289<style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 11290.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} 11291 11292.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)}} 11293 11294.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} 11295 11296.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:#EAB308;margin:0} 11297 11298.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)} 11299 11300.pd-share-icon{width:40px;height:40px;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;position:relative}.pd-share-icon svg{width:24px;height:24px;display:block}.pd-share-icon-whatsapp{background:linear-gradient(135deg,#25D366 0%,#128C7E 100%);color:#fff}.pd-share-icon-facebook{background:linear-gradient(135deg,#1877F2 0%,#0c63d4 100%);color:#fff}.pd-share-icon-twitter{background:linear-gradient(135deg,#1DA1F2 0%,#0d8bd9 100%);color:#fff}.pd-share-icon-telegram{background:linear-gradient(135deg,#0088cc 0%,#0077b3 100%);color:#fff}.pd-share-icon-email{background:linear-gradient(135deg,#EA4335 0%,#c5331f 100%);color:#fff}.pd-share-icon-copy{background:linear-gradient(135deg,#FCD34D 0%,#FBBF24 100%);color:#fff}.pd-share-icon-pdf{background:linear-gradient(135deg,#ef4444 0%,#dc2626 100%);color:#fff}.pd-share-name{font-size:12px;font-weight:600;color:#475569;text-align:center} 11301 11302.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:#EAB308;box-shadow:0 0 0 3px rgba(201,162,39,.1)}.pd-share-copy-btn{padding:12px 20px;border:0;border-radius:10px;background:linear-gradient(135deg,#EAB308 0%,#A16207 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,#A16207 0%,#92400E 100%);transform:translateY(-1px);box-shadow:0 4px 12px rgba(201,162,39,.3)}.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} 11303 11304@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}} 11305 11306.mf-share{background:#fff;color:#FBBF24;border:2px solid #FBBF24;display:inline-flex;align-items:center;justify-content:center}.mf-share:active{transform:scale(.95)}.mf-share svg{display:block} 11307 11308@media(max-width:640px){.pd-share-btn{display:none!important}} 11309 11310.stock-badge.in{background:#e6f9ec;color:#15803d;border-radius:999px;padding:4px 10px;font-weight:700}.stock-badge.out{background:#EAB308;color:#14b1d7;border-radius:999px;padding:4px 10px;font-weight:700} 11311 11312.stock-info-btn{--c:#EAB308;--b:#FDE68A;--bg:#FFFBEB;--bg-h:#FEF9C3;--b-h:#EAB308;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:#A16207;transform:translateY(-1px);box-shadow:0 2px 8px rgba(201,162,39,.15)}.stock-info-btn:active{transform:translateY(0)}.stock-info-btn:focus-visible{outline:0;box-shadow:0 0 0 3px rgba(201,162,39,.2)}.stock-info-btn svg{width:14px;
11312height:14px;flex-shrink:0}.stock-info-btn .btn-text{white-space:nowrap} 11313 11314.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} 11315 11316.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,#FBBF24);display:inline-block}.sp-label{font-weight:600;font-size:13px} 11317 11318 11319.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} 11320 11321.sp-wa{ 11322 grid-column:3; justify-self:end; 11323 width:32px; height:32px; min-width:32px; min-height:32px; 11324 box-sizing:border-box; 11325 padding:0; line-height:0; text-decoration:none; outline:0; 11326 display:inline-flex; align-items:center; justify-content:center; 11327 border-radius:999px; border:1px solid rgba(37,211,102,.60); /* 1px untuk hasil âcrispâ */ 11328 background:rgba(37,211,102,.08); color:#25D366; 11329 transition:background-color .15s ease, border-color .15s ease, transform .05s ease; 11330 -webkit-tap-highlight-color:transparent; 11331} 11332.sp-wa:hover{background:rgba(37,211,102,.16);border-color:rgba(37,211,102,.75)} 11333.sp-wa:active{transform:translateY(.5px) scale(.98)} 11334 11335.sp-wa svg{width:18px;height:18px;display:block;vertical-align:middle} 11336.stock-popover a{text-decoration:none} 11337 11338#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}} 11339 11340.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:#CA8A04}.stock-dot.out{background:#ef4444}.stock-dot:hover{transform:scale(1.2);transition:transform 0.2s ease}@media(max-width:640px){.stock-dot{width:7px;height:7px;bottom:6px;right:6px}} 11341 11342.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}} 11343 11344.pwp-trigger-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 4px;background:linear-gradient(135deg,#EAB308 0%,#CA8A04 100%);color:#78350F;border-radius:6px;font-size:.65rem;font-weight:600;border:0}.pwp-trigger-badge svg{flex-shrink:0;width:12px;height:12px} 11345.pwp-deals-section{margin:0;padding:12px;background:linear-gradient(135deg,#FEF3C7 0%,#FDE68A 100%);border-radius:12px;border:2px solid #EAB308}.pwp-deals-header{display:flex;gap:10px;margin-bottom:10px}.pwp-icon{flex-shrink:0;color:#CA8A04;width:18px;height:18px}.pwp-text .pwp-title{margin:0 0 2px 0;font-size:14px;font-weight:700;color:#78350F}.pwp-text .pwp-subtitle{margin:0;font-size:11px;color:#8B6914;opacity:0.9}.pwp-deals-loading{text-align:center;padding:40px 0;color:#8B6914}.pwp-spinner{width:40px;height:40px;border:3px solid #EAB308;border-top-color:#CA8A04;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 12px}@keyframes spin{to{transform:rotate(360deg)}} 11346.pwp-deals-grid{display:flex;flex-direction:column;gap:6px}.pwp-deal-card{background:#fff;border-radius:6px;overflow:hidden;border:1px solid #EAB308;display:flex;align-items:center;padding:8px;gap:10px}.pwp-deal-card:hover{background:#FEF3C7}.pwp-deal-thumb{flex-shrink:0;width:50px;height:50px;background:#f8fafc;overflow:hidden;border-radius:4px}.pwp-deal-thumb img{width:100%;height:100%;object-fit:cover}.pwp-deal-badge{background:#dc2626;color:#fff;padding:2px 5px;border-radius:3px;font-size:9px;font-weight:700;margin-left:auto}.pwp-deal-info{flex:1;display:flex;flex-direction:column;gap:4px}.pwp-deal-name{font-size:12px;font-weight:600;color:#0f172a;margin:0;line-height:1.2}.pwp-deal-prices{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.pwp-deal-price{font-size:14px;font-weight:800;color:#dc2626;white-space:nowrap}.pwp-deal-original{font-size:11px;color:#64748b;text-decoration:line-through;white-space:nowrap}.pwp-cart-note{margin-top:8px;padding:6px 10px;background:#FEF9C3;border-left:2px solid#EAB308;font-size:11px;color:#0c4a6e;border-radius:4px} 11347@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}} 11348 11349.preorder-badge{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;background:linear-gradient(135deg,#CA8A04 0%,#8B6914 100%);color:#fff;border-radius:8px;font-size:13px;font-weight:700;border:0;box-shadow:0 2px 8px rgba(234,179,8,.25)}.preorder-badge svg{width:16px;height:16px;flex-shrink:0}.preorder-info{margin:12px 0;padding:14px;background:linear-gradient(135deg,#FFFBEB 0%,#FEF3C7 100%);border:1.5px solid #FDE68A;border-radius:12px}.preorder-info-header{display:flex;align-items:center;gap:8px;margin-bottom:10px}.preorder-info-icon{width:20px;height:20px;color:#8B6914;flex-shrink:0}.preorder-info-title{font-size:14px;font-weight:700;color:#78350F;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:#8B6914;font-weight:600}.preorder-info-value{color:#78350F;font-weight:700}.preorder-info-value.highlight{color:#dc2626;font-size:14px}.preorder-info-note{margin-top:10px;padding:8px 10px;background:#FEF3C7;border-left:2px solid #CA8A04;font-size:12px;color:#78350F;border-radius:6px;line-height:1.4}@media(max-width:640px){.preorder-badge{font-size:12px;padding:5px 10px}.preorder-info{margin:10px 0;padding:12px}.preorder-info-title{font-size:13px}.preorder-info-row{font-size:12px}.preorder-info-note{font-size:11px;padding:6px 8px}} 11350</style> 11351 11352<div class="pd-sticky-header" id="pd-sticky-header"><div class="pd-sticky-header-container"><div class="pd-sticky-thumb"><img src="img/banner-edutoto.webp" alt="EDUTOTO" id="pd-sticky-thumb-img"></div><div class="pd-sticky-info"><h2 class="pd-sticky-title" id="pd-sticky-title">EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap</h2><div class="pd-sticky-price-row"><span class="pd-sticky-price" id="pd-sticky-price">Rp 10.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">
11352<path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></button><button type="button" class="pd-sticky-btn pd-sticky-btn-chat" id="pd-sticky-chat"><svg viewBox="0 0 640 640" fill="currentColor" aria-hidden="true"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg>Tanya</button><button type="button" class="pd-sticky-btn pd-sticky-btn-cart" id="pd-sticky-add-cart" disabled>+ Keranjang</button><button type="button" class="pd-sticky-btn pd-sticky-btn-buy" id="pd-sticky-buy" disabled>Beli Sekarang</button></div></div></div> 11353 11354<main class="product-single product-single--v3"><nav class="pd-breadcrumb" aria-label="breadcrumb"><a href="https://norita.mx/">EDUTOTO</a><span>-</span><a href="https://norita.mx/">EDUTOTO LOGIN</a><span>-</span><a href="https://norita.mx/">ALTERNATIF EDUTOTO</a><span>-</span><a href="https://norita.mx/">EDUTOTO DAFTAR</a><span>-</span><a href="https://norita.mx/">WEBSITE EDUTOTO</a><span>-</span><span class="current">EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap 11355 11356</span></nav> 11357 11358<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="img/banner-edutoto.webp" alt="SITUS SLOT" data-slug="" loading="eager"> 11359</figure> 11360 11361<div class="pd-mobile-slider-wrapper"> 11362<div class="pd-mobile-slider" id="pd-mobile-slider" aria-label="Galeri gambar (geser)"><div class="pd-mslide"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="eager"></div><div class="pd-mslide"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></div><div class="pd-mslide"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></div><div class="pd-mslide"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></div><div class="pd-mslide"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></div><div class="pd-mslide"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></div></div><div class="pd-gallery-counter" id="pd-gallery-counter">1/6</div></div> 11363<div class="pd-thumb-gallery" id="pd-thumb-gallery" aria-label="Galeri gambar produk"><button type="button" class="pd-thumb-btn active" data-full="img/banner-edutoto.webp" data-alt="EDUTOTO LOGIN" data-slug="" aria-label="Gambar 1"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="img/banner-edutoto.webp" data-alt="EDUTOTO LOGIN" data-slug="img/banner-edutoto.webp" aria-label="Gambar 2"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="img/banner-edutoto.webp" data-alt="EDUTOTO LOGIN" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp" aria-label="Gambar 3"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="img/banner-edutoto.webp" data-alt="EDUTOTO LOGIN" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp" aria-label="Gambar 4"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="img/banner-edutoto.webp" data-alt
11363="EDUTOTO LOGIN" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp" aria-label="Gambar 5"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="img/banner-edutoto.webp" data-alt="EDUTOTO LOGIN" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-892.webp" aria-label="Gambar 6"><img src="img/banner-edutoto.webp" alt="EDUTOTO LOGIN" 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"> 11364 <h1 class="pd-title" id="pd-title">EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap</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> 11365<div id="pd-price-anchor"> 11366</div> 11367<div class="pd-price-qr-container"> 11368 <div class="pd-price-block" id="pd-price-block"> 11369 <div class="pd-price-row"> 11370 <span class="pd-price-current" id="pd-price-current" >Rp 10.000</span> 11371 <span class="pd-price-original" id="pd-price-original">Rp 50.000</span> 11372 <span class="pd-discount-badge" id="pd-discount-badge">-90%</span></div> 11373 <div class="pd-meta-row"> 11374 <span class="sold-inline" id="pd-sold-inline" data-role="sold-chip">Terjual Terjual 5</span> 11375 <span class="pwp-trigger-badge" id="pwp-trigger-badge" hidden> 11376 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 11377 <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> 11378 <div class="pd-meta-inline"><span class="stock-badge out" id="pd-stock-badge">EDUTOTO</span> 11379 <span class="sku">SKU: <span id="pd-sku">EDUTOTO</span></span></div> 11380<div class="paylater-promo paylater-promo-sm paylater-mobile-only"> 11381 <span class="paylater-icon">ð³</span> 11382 <span class="paylater-text"> 11383 <strong>SLOT</strong> 11384 <small>ONLINE</small> 11385 </span> 11386</div> 11387 11388<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"> 11389<li class="sp-item"><div class="sp-left"><span class="sp-dot" style="--dot:#FBBF24;"></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-8asp2-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">
11389<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> 11390 11391 <li class="sp-item"> 11392 <div class="sp-left"> 11393 <span class="sp-dot" style="--dot:#34d399;"></span> 11394 <span class="sp-label">Medan Fair</span> 11395 </div> 11396 <span class="sp-qty" id="sp-qty-mdf">0</span> 11397 <a class="sp-wa" 11398 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" 11399 target="_blank" rel="noopener" aria-label="WhatsApp Medan Fair"> 11400 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11401 <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"/> 11402 </svg> 11403 </a> 11404 </li> 11405 11406 <li class="sp-item"> 11407 <div class="sp-left"> 11408 <span class="sp-dot" style="--dot:#CA8A04;"></span> 11409 <span class="sp-label">Banda Aceh</span> 11410 </div> 11411 <span class="sp-qty" id="sp-qty-bda">0</span> 11412 <a class="sp-wa" 11413 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" 11414 target="_blank" rel="noopener" aria-label="WhatsApp Banda Aceh"> 11415 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11416 <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"/> 11417 </svg> 11418 </a> 11419 </li> 11420 </ul> 11421 </div> 11422 11423 </div> 11424 <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-8as
11424p2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" alt="QR Code" class="pd-qr-image"></div> 11425 </div> 11426 <div id="pd-variants-anchor"></div> 11427 11428 11429 11430 <div class="pd-compare-section"> 11431 <button type="button" class="pd-compare-btn" id="pd-compare-btn" data-product-id="325"> 11432 <svg class="compare-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11433 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 11434 </svg> 11435 <span class="compare-text">Bandingkan Produk</span> 11436 <span class="compare-remove" hidden> 11437 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11438 <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"/> 11439 </svg> 11440 Hapus dari Perbandingan 11441 </span> 11442 </button> 11443 <div class="pd-compare-info"> 11444 <small class="compare-note" id="compare-note">Bandingkan hingga 5 produk dengan bantuan AI</small> 11445 </div> 11446 </div> 11447 11448 <div class="pd-short-desc"><p><strong>Informasi EDUTOTO:</strong></p><ul><li>Minimal Deposit: Rp.10.000,-</li><li>Jenis Permainan: EDUTOTO LOGIN</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: 328.328.328</li><li>Rating: âââââ</li><li>Daftar Buruan: <a href="https://relaksasi.norita.mx/">Klik Disini</a></li></ul></div><div class="EDUTOTO-container-v1 mini"> 11449<div class="EDUTOTO-container"> 11450 11451 <!-- HEADER --> 11452<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;800&family=Rajdhani:wght@500;700&display=swap" rel="stylesheet"> 11453 11454<div class="rtp-widget"> 11455 11456 <div class="rtp-header"> 11457 11458 <div class="title"> 11459 11460 <span class="live-text">LIVE RTP MONITOR</span> 11461 11462 <h1>LIVE RTP EDUTOTO</h1> 11463 11464 </div> 11465 11466 <div class="update-box"> 11467 11468 <div class="update-item"> 11469 11470 <span>Update terakhir</span> 11471 11472 <strong id="lastUpdate">--:-- WIB</strong> 11473 11474 </div> 11475 11476 <div class="update-item"> 11477 11478 <span>Refresh berikutnya</span> 11479 11480 <strong id="nextUpdate">60 detik</strong> 11481 11482 </div> 11483 11484 </div> 11485 11486 </div> 11487 11488 <div class="cards"> 11489 11490 <div class="card" data-index="0"> 11491 11492 <div class="card-header"> 11493 11494 <h3>PRAGMATIC PLAY</h3> 11495 11496 <span>LIVE</span> 11497 11498 </div> 11499 11500 <div class="rtp"> 11501 11502 <strong class="value">0.00</strong> 11503 11504 <small>%</small> 11505 11506 </div> 11507 11508 <div class="bar"> 11509 11510 <div class="fill"></div> 11511 11512 </div> 11513 11514 <div class="footer"> 11515 11516 <span class="status">NORMAL</span> 11517 11518 <span class="trend">+0.00</span> 11519 11520 </div> 11521 11522 </div> 11523 11524 <div class="card" data-index="1"> 11525 11526 <div class="card-header"> 11527 11528 <h3>PG SOFT</h3> 11529 11530 <span>LIVE</span> 11531 11532 </div> 11533 11534 <div class="rtp"> 11535 11536 <strong class="value">0.00</strong> 11537 11538 <small>%</small> 11539 11540 </div> 11541 11542 <div class="bar"> 11543 11544 <div class="fill"></div> 11545 11546 </div> 11547 11548 <div class="footer"> 11549 11550 <span class="status">NORMAL</span> 11551 11552 <span class="trend">+0.00</span> 11553 11554 </div> 11555 11556 </div> 11557 11558 <div class="card" data-index="2"> 11559 11560 <div class="card-header"> 11561 11562 <h3>JOKER123</h3> 11563 11564 <span>LIVE</span> 11565 11566 </div> 11567 11568 <div class="rtp"> 11569 11570 <strong class="value">0.00</strong> 11571 11572 <small>%</small> 11573 11574 </div> 11575 11576 <div class="bar"> 11577 11578 <div class="fill"></div> 11579 11580 </div> 11581 11582 <div class="footer"> 11583
11584 <span class="status">NORMAL</span> 11585 11586 <span class="trend">+0.00</span> 11587 11588 </div> 11589 11590 </div> 11591 11592 <div class="card" data-index="3"> 11593 11594 <div class="card-header"> 11595 11596 <h3>MICROGAMING</h3> 11597 11598 <span>LIVE</span> 11599 11600 </div> 11601 11602 <div class="rtp"> 11603 11604 <strong class="value">0.00</strong> 11605 11606 <small>%</small> 11607 11608 </div> 11609 11610 <div class="bar"> 11611 11612 <div class="fill"></div> 11613 11614 </div> 11615 11616 <div class="footer"> 11617 11618 <span class="status">NORMAL</span> 11619 11620 <span class="trend">+0.00</span> 11621 11622 </div> 11623 11624 </div> 11625 11626 </div> 11627 11628</div> 11629 11630 <style> 11631@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;800&family=Rajdhani:wght@500;700&display=swap'); 11632 11633*{ 11634 margin:0; 11635 padding:0; 11636 box-sizing:border-box; 11637} 11638 11639.rtp-widget{ 11640 width:100%; 11641 max-width:430px; 11642 margin:20px auto; 11643 padding:18px; 11644 border-radius:24px; 11645 background:linear-gradient(180deg,#14131d,#0f1018); 11646 border:1px solid rgba(201,162,39,.28); 11647 box-shadow: 11648 0 0 35px rgba(201,162,39,.28), 11649 inset 0 1px rgba(255,255,255,.05); 11650 overflow:hidden; 11651} 11652 11653.rtp-header{ 11654 margin-bottom:18px; 11655} 11656 11657.live-text{ 11658 display:block; 11659 color:#FBBF24; 11660 font-size:12px; 11661 font-weight:700; 11662 letter-spacing:3px; 11663 margin-bottom:10px; 11664} 11665 11666.title h1{ 11667 font-family:'Orbitron',sans-serif; 11668 font-size:27px; 11669 line-height:1.05; 11670 color:#fff; 11671 margin-bottom:18px; 11672} 11673 11674.update-box{ 11675 display:flex; 11676 justify-content:space-between; 11677 gap:10px; 11678 padding:12px; 11679 background:rgba(255,255,255,.05); 11680 border:1px solid rgba(255,255,255,.05); 11681 border-radius:14px; 11682} 11683 11684.update-item{ 11685 display:flex; 11686 flex-direction:column; 11687} 11688 11689.update-item span{ 11690 color:#bdbdbd; 11691 font-size:11px; 11692} 11693 11694.update-item strong{ 11695 margin-top:4px; 11696 color:#fff; 11697 font-family:'Orbitron'; 11698 font-size:13px; 11699} 11700 11701.cards{ 11702 display:grid; 11703 grid-template-columns:repeat(2,1fr); 11704 gap:14px; 11705} 11706 11707.card{ 11708 position:relative; 11709 padding:14px; 11710 border-radius:16px; 11711 background:linear-gradient(180deg,#222836,#1a202b); 11712 border:1px solid rgba(255,255,255,.08); 11713 transition:.3s; 11714} 11715 11716.card:hover{ 11717 transform:translateY(-4px); 11718 box-shadow:0 12px 25px rgba(0,0,0,.35); 11719} 11720 11721.card:nth-child(1), 11722.card:nth-child(2), 11723.card:nth-child(3), 11724.card:nth-child(4){ 11725 border-top:3px solid #C9A227; 11726} 11727 11728.card-header{ 11729 display:flex; 11730 justify-content:space-between; 11731 align-items:flex-start; 11732 margin-bottom:18px; 11733} 11734 11735.card-header h3{ 11736 width:70%; 11737 font-size:12px; 11738 line-height:1.2; 11739 color:#fff; 11740 font-family:'Orbitron'; 11741} 11742 11743.card-header span{ 11744 padding:4px 8px; 11745 border-radius:20px; 11746 background:#12161f; 11747 color:#FBBF24; 11748 font-size:9px; 11749 font-weight:bold; 11750} 11751 11752.rtp{ 11753 display:flex;
11754 align-items:flex-end; 11755 gap:4px; 11756 margin-bottom:14px; 11757} 11758 11759.value{ 11760 font-family:'Orbitron'; 11761 font-size:38px; 11762 line-height:1; 11763} 11764 11765.rtp small{ 11766 color:#FBBF24; 11767 font-size:18px; 11768 font-family:'Orbitron'; 11769 margin-bottom:3px; 11770} 11771 11772.bar{ 11773 width:100%; 11774 height:8px; 11775 background:#434857; 11776 border-radius:20px; 11777 overflow:hidden; 11778} 11779 11780.fill{ 11781 width:0%; 11782 height:100%; 11783 border-radius:20px; 11784 background:linear-gradient(90deg,#8B6914,#C9A227,#FBBF24,#FDE68A); 11785 transition:.8s; 11786} 11787 11788.footer{ 11789 margin-top:14px; 11790 display:flex; 11791 justify-content:space-between; 11792 align-items:center; 11793} 11794 11795.status{ 11796 font-size:12px; 11797 color:#FBBF24; 11798 font-weight:700; 11799} 11800 11801.trend{ 11802 font-size:12px; 11803 color:#fff; 11804 font-weight:700; 11805} 11806 11807@media(max-width:420px){ 11808 11809.rtp-widget{ 11810 padding:15px; 11811} 11812 11813.title h1{ 11814 font-size:24px; 11815} 11816 11817.value{ 11818 font-size:32px; 11819} 11820 11821.card-header h3{ 11822 font-size:11px; 11823} 11824 11825} 11826 </style> 11827
11828<script> 11829const providers = [ 11830 { name: "PRAGMATIC PLAY", min: 86, max: 98 }, 11831 { name: "PG SOFT", min: 84, max: 97 }, 11832 { name: "JOKER123", min: 82, max: 96 }, 11833 { name: "MICROGAMING", min: 83, max: 97 } 11834]; 11835 11836const cards = document.querySelectorAll(".card"); 11837const lastUpdate = document.getElementById("lastUpdate"); 11838const nextUpdate = document.getElementById("nextUpdate"); 11839 11840let countdown = 60; 11841 11842function seededRandom(seed){ 11843 const x = Math.sin(seed) * 10000; 11844 return x - Math.floor(x); 11845} 11846 11847function statusRtp(value){ 11848 11849 if(value >= 95){ 11850 return { 11851 text:"PANAS", 11852 color:"#FDE68A" 11853 }; 11854 } 11855 11856 if(value >= 89){ 11857 return { 11858 text:"NORMAL", 11859 color:"#FBBF24" 11860 }; 11861 } 11862 11863 return{ 11864 text:"TURUN", 11865 color:"#CA8A04" 11866 }; 11867 11868} 11869 11870function updateCards(){ 11871 11872 const now = new Date(); 11873 11874 lastUpdate.innerHTML = now.toLocaleTimeString("id-ID",{ 11875 hour:"2-digit", 11876 minute:"2-digit" 11877 })+" WIB"; 11878 11879 cards.forEach((card,index)=>{ 11880 11881 const p = providers[index]; 11882 11883 const rand = seededRandom(Date.now()/60000+(index*15)); 11884 11885 const value = ( 11886 p.min + rand * (p.max-p.min) 11887 ); 11888 11889 const percent = value.toFixed(2); 11890 11891 card.querySelector(".value").innerHTML = percent; 11892 11893 card.querySelector(".fill").style.width = percent+"%"; 11894 11895 const trend = (Math.random()*2-1).toFixed(2); 11896 11897 const trendEl = card.querySelector(".trend"); 11898 11899 trendEl.innerHTML = 11900 (trend>=0?"+":"")+trend; 11901 11902 trendEl.style.color = 11903 trend>=0 11904 ? "#FCD34D" 11905 : "#A16207"; 11906 11907 const st = statusRtp(value); 11908 11909 const status = card.querySelector(".status"); 11910 11911 status.innerHTML = st.text; 11912 11913 status.style.color = st.color; 11914 11915 }); 11916 11917} 11918 11919updateCards(); 11920 11921setInterval(()=>{ 11922 11923 countdown--; 11924 11925 nextUpdate.innerHTML = 11926 countdown+" detik"; 11927 11928 if(countdown<=0){ 11929 11930 countdown=60; 11931 11932 updateCards(); 11933 11934 } 11935 11936},1000); 11937 11938</script>
11938 11939
11940<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 11940
11940
11941<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 11941
11941
11942<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 11942
11942
11943<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 11943
11943
11944<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 11944
11944
11945<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 11945
11945
11946<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>
11946 11947 11948<style> 11949body{background:#FFFBEB; font-family:Poppins,sans-serif; color:white;} 11950.EDUTOTO-container{ 11951max-width: 520px; 11952 margin: auto; 11953 padding: 15px; 11954 border-radius: 12px; 11955 background: #1a1508; 11956 border: 2px solid #EAB308; 11957 box-shadow: 0 0 20px #EAB308; 11958} 11959 11960.EDUTOTO-header{ 11961 font-weight:900; 11962 text-align:center; 11963 color:#FBBF24; 11964 text-shadow:0 0 12px #C9A227; 11965 margin-bottom:12px; 11966} 11967 11968.EDUTOTO-controls{ 11969 display:grid; 11970 grid-template-columns:1fr 1fr; 11971 gap:10px; 11972 margin-bottom:12px; 11973} 11974.EDUTOTO-controls label{ 11975 display:flex; flex-direction:column; 11976 font-size:11px; color:#FDE68A; 11977} 11978.EDUTOTO-controls select, 11979.EDUTOTO-controls input{ 11980 padding:8px; 11981 border-radius:8px; 11982 border:1px solid #FBBF24; 11983 background:#1a1508; 11984 color:white; 11985} 11986#btnGenerate{ 11987 grid-column:span 2; 11988 padding:12px; 11989 background:#C9A227; 11990 border:none; 11991 color:#1a1508; 11992 font-weight:900; 11993 cursor:pointer; 11994 margin-top:5px; 11995 box-shadow:0 0 15px #EAB308; 11996} 11997 11998.EDUTOTO-result{ 11999 margin-top:12px; 12000 padding:12px; 12001 border-radius:10px; 12002 background:rgba(234,179,8,0.05); 12003 border:1px solid #FBBF24; 12004} 12005 12006.EDUTOTO-main div, .EDUTOTO-details div{ 12007 margin:5px 0; 12008} 12009 12010#rPrediction{ 12011 font-size:24px; 12012 font-weight:900; 12013 color:#FBBF24; 12014} 12015 12016.EDUTOTO-alts{ 12017 margin-top:10px; 12018} 12019.EDUTOTO-alts h4{margin-bottom:6px;} 12020#EDUTOTOAltList div{ 12021 background:#1a1508; 12022 padding:6px 10px; 12023 margin:3px 0; 12024 border-radius:8px; 12025 border-left:3px solid #C9A227; 12026} 12027.bank-tab.active, .bank-tab:hover { 12028 background: #EAB308; 12029 color: #fff; 12030 border-color: #EAB308; 12031 box-shadow: 0 3px 10px -4px rgb(234 179 8 / 60%); 12032} 12033 12034.EDUTOTO-disclaimer{ 12035 font-size:10px; 12036 margin-top:8px; 12037 text-align:center; 12038 color:#FDE68A; 12039} 12040</style> 12041
12042<script> 12043const leagues = ["Premier League","La Liga","Serie A"]; 12044const tips = ["Analisis statistik tim","Performa terakhir","Cek head-to-head"]; 12045function rand(arr){return arr[Math.floor(Math.random()*arr.length)];} 12046 12047var _bgBtn=document.getElementById("btnGenerate");if(_bgBtn)_bgBtn.onclick=function(){ 12048 const type = document.getElementById("betTypeFilter").value; 12049 const league = rand(leagues); 12050 12051 let pred; 12052 if(type==="1X2") pred=rand(["1","X","2"]); 12053 else if(type==="OVER_UNDER") pred=rand(["Over","Under"]); 12054 else pred=rand(["Home -1","Away +1","Draw"]); 12055 12056 document.getElementById("rLeague").innerText=league; 12057 document.getElementById("rPrediction").innerText=pred; 12058 document.getElementById("rBetType").innerText=type; 12059 document.getElementById("rConf").innerText=Math.floor(Math.random()*40+60)+"%"; 12060 document.getElementById("rTip").innerText=rand(tips); 12061 document.getElementById("rOdds").innerText=(Math.random()*3+1).toFixed(2); 12062 12063 // alternatif 12064 const alt=document.getElementById("EDUTOTOAltList"); alt.innerHTML=""; 12065 for(let i=0;i<3;i++){let d=document.createElement("div"); d.innerText=rand(["1","X","2"]); alt.appendChild(d);} 12066 12067 document.getElementById("EDUTOTOStatus").innerText="â READY â"; 12068} 12069</script>
12069 12070 12071 12072 12073 <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> 12074 </div> 12075 </div> 12076 </section> 12077 12078 <aside class="pd-sidebar"> 12079 <div class="pd-side-card pd-buy-card"> 12080 <form class="pd-buy-form" id="pd-buy-form" data-pid="325"> 12081 <input type="hidden" name="variant_id" id="pd-variant-id" value="0"> 12082 <div> 12083 <label for="qty" class="qty-label">Qty</label> 12084 <input type="number" id="qty" name="qty" value="0" min="1" 12085 max="0" 12086 disabled> 12087 </div> 12088 <div class="pd-actions"> 12089 <button type="submit" class="btn-add-cart" id="btn-add-cart" disabled>+ Keranjang</button> 12090 <button type="button" class="btn-buy" id="btn-buy" data-id="278426" disabled >Beli</button> 12091 <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> 12092 </div> 12093 </form> 12094 <div class="paylater-promo paylater-promo-md paylater-desktop-only"> 12095 <span class="paylater-icon">ð³</span> 12096 <span class="paylater-text"> 12097 <strong>Autorized EDUTOTO</strong> 12098 <small>Situs Resmi EDUTOTO </small> 12099 </span> 12100</div> </div> 12101 12102 12103 <div class="pd-side-card qoala-banner-card"> 12104 <div class="qoala-banner"> 12105 <div class="qoala-banner-content"> 12106 <div class="qoala-icon"> 12107 <center><img src="/img/favicon.png" alt="EDUTOTO" class="qoala-shield-img"></center> 12108 </div> 12109 <div class="qoala-text"> 12110 <div class="qoala-title">Login EDUTOTO</div> 12111 <div class="qoala-subtitle">Verified</div> 12112 </div> 12113 </div> 12114 <a href="https://relaksasi.norita.mx/" class="qoala-cta" 12115 data-ga-event="qoala_banner_click" 12116 data-product-id="325" 12117 data-product-name="EDUTOTO" 12118 data-category-path="Komputer & Laptop/Laptop/Gaming" target="_blank">Info</a> 12119 </div> 12120<style> 12121.qoala-banner-card{ 12122 padding:0; 12123 overflow:hidden; 12124} 12125 12126.qoala-banner{ 12127 background:linear-gradient(135deg, #EAB308 0%, #FBBF24 100%); 12128 padding:12px 16px; 12129 display:flex; 12130 align-items:center; 12131 justify-content:space-between; 12132 color:white; 12133 text-decoration:none; 12134 min-height:60px; 12135} 12136 12137.qoala-banner-content{ 12138 display:flex; 12139 align-items:center; 12140 gap:12px; 12141} 12142 12143.qoala-icon{ 12144 display:flex; 12145 align-items:center; 12146 justify-content:center; 12147} 12148 12149.qoala-shield-img{ 12150 width:36px; 12151 height:36px; 12152 object-fit:contain; 12153 opacity:.9; 12154} 12155 12156.qoala-title{ 12157 font-weight:600; 12158 font-size:14px; 12159 line-height:1.2; 12160} 12161 12162.qoala-subtitle{ 12163 font-size:12px; 12164 opacity:.9; 12165 line-height:1.2; 12166} 12167 12168.qoala-cta{ 12169 background:rgba(255,255,255,.15); 12170 color:white; 12171 padding:6px 12px; 12172 border-radius:6px; 12173 font-size:12px; 12174 font-weight:600; 12175 text-decoration:none; 12176 transition:background .2s ease; 12177 border:1px solid rgba(234,179,8,.4); 12178} 12179 12180.qoala-cta:hover{ 12181 background:rgba(251,191,36,.25); 12182 color:white; 12183} 12184 12185@media(max-width:768px){ 12186 .qoala-banner{ 12187 padding:10px 12px; 12188 } 12189 12190 .qoala-title{ 12191 font-size:17px; 12192 } 12193 12194 .qoala-subtitle{ 12195 font-size:11px; 12196 } 12197} 12198</style> 12199 </div> 12200 12201 <div class="pd-side-card pd-installment-card2" 12202 id="inst-card" 12203 data-base-price="20789000" 12204 data-zero-smart="0"> 12205 <div class="inst-head"> 12206 <h3>Promo & Fitur EDUTOTO</h3> 12207 </div> 12208 12209 <div class="inst-bank-tabs" role="tablist" aria-label="Bank"> 12210 <button type="button" class="bank-tab active" data-bank="Lucky Draw" aria-selected="true">Lucky Draw</button> 12211 <button type="button" class="bank-tab" data-bank="Lomba Panjat Tur
12211nover" aria-selected="false">Lomba Panjat Turnover</button> 12212 <button type="button" class="bank-tab" data-bank="RTP Gacor" aria-selected="false">RTP Gacor</button> 12213 </div> 12214 12215 <ul class="inst-month-list" id="inst-month-list" aria-label="Daftar cicilan per bulan"></ul> 12216 <div class="inst-note" id="inst-zero-note" style="display:none;">0% Cicilan khusus bank terpilih*</div> 12217 </div> 12218 12219 <!-- MECHA GOLD FINAL BOSS REVIEW SLIDER --> 12220<div class="tnt-review-mecha"> 12221 12222<div class="tnt-header"> 12223<h2>REVIEW MEMBER EDUTOTO</h2> 12224 12225</div> 12226 12227<div class="tnt-slider" id="tntSlider"> 12228 12229 <div class="tnt-slide active"> 12230 <div class="tnt-name">Raka Putra â Bandung</div> 12231 <div class="tnt-comment">"Sejak pertama masuk, EDUTOTO terasa nyaman. Menu-menunya rapi, aksesnya cepat, dan saya bisa langsung bermain tanpa ribet."</div> 12232</div> 12233 12234<div class="tnt-slide"> 12235 <div class="tnt-name">Nadia Kusuma â Surabaya</div> 12236 <div class="tnt-comment">"Tampilannya elegan dan tidak bikin pusing. Saya main di HP setiap hari, alurnya ringan, dan suasana relaksasinya benar-benar terasa."</div> 12237</div> 12238 12239<div class="tnt-slide"> 12240 <div class="tnt-name">Fajar Santoso â Yogyakarta</div> 12241 <div class="tnt-comment">"Yang bikin saya betah di EDUTOTO adalah sistemnya stabil dan fiturnya lengkap. Login sampai transaksi terasa praktis, jadi mainnya lebih tenang."</div> 12242</div> 12243 12244 12245 12246 12247</div> 12248</div> 12249 12250</aside> 12251</div> 12252 12253<style> 12254.tnt-review-mecha{ 12255 background: radial-gradient(circle at center, #EAB308, #1a1508); 12256 border: 2px solid #CA8A04; 12257 border-radius: 14px; 12258 padding: 20px; 12259 color: #dbdbdb; 12260 font-family: 'Orbitron', sans-serif; 12261 position: relative; 12262 overflow: hidden; 12263 box-shadow: 0 0 25px #EAB308, inset 0 0 20px rgba(234, 179, 8, 0.2); 12264} 12265 12266.tnt-review-mecha::before{ 12267 content:""; 12268 position:absolute; 12269 inset:0; 12270 background: repeating-linear-gradient( 12271 to bottom, 12272 rgba(234,179,8,0.08), 12273 rgba(234,179,8,0.08) 1px, 12274 transparent 2px, 12275 transparent 4px 12276 ); 12277 pointer-events:none; 12278} 12279 12280.bank-tab.active, 12281.bank-tab:hover 12282 12283.tnt-header h2{ 12284 font-size:18px; 12285 color:#000000; 12286 text-shadow:0 0 10px #FBBF24, 0 0 20px #EAB308; 12287 margin-bottom:5px; 12288} 12289 12290.tnt-header p{ 12291 font-size:12px; 12292 opacity:.85; 12293 margin-bottom:15px; 12294} 12295 12296.tnt-slider{ 12297 position:relative; 12298 height:90px; 12299} 12300 12301.tnt-slide{ 12302 position:absolute; 12303 width:100%; 12304 opacity:0; 12305 transform:translateX(40px); 12306 transition:all .6s ease; 12307} 12308 12309.tenor-badge{ 12310 background:#EAB308; 12311 color:#fff; 12312 font-weight:700; 12313 min-width:42px; 12314 text-align:center; 12315 padding:.38rem .4rem; 12316 font-size:.8rem; 12317 border-radius:10px; 12318 box-shadow:0 3px 10px -4px rgb(234 179 8); 12319} 12320 12321.tnt-slide.active{ 12322 opacity:1; 12323 transform:translateX(0); 12324} 12325 12326.tnt-name{ 12327 font-size:14px; 12328 color:#000000; 12329 margin-bottom:5px; 12330 text-shadow:0 0 6px #5a0080; 12331} 12332 12333.tnt-comment{ 12334 font-size:13px; 12335 line-height:1.5; 12336} 12337 12338@keyframes pulseGlow{ 12339 0%{box-shadow:0 0 10px #EAB308;} 12340 50%{box-shadow:0 0 25px #FBBF24;} 12341 100%{box-shadow:0 0 10px #EAB308;} 12342} 12343 12344.tnt-review-mecha{ 12345 animation:pulseGlow 2.5s infinite; 12346} 12347</style> 12348
12349<script> 12350let slides = document.querySelectorAll('.tnt-slide'); 12351let index = 0; 12352 12353setInterval(()=>{ 12354 slides[index].classList.remove('active'); 12355 index = (index + 1) % slides.length; 12356 slides[index].classList.add('active'); 12357}, 3000); 12358</script>
12358 12359 12360 <div class="artikel-glow-santaaa"> 12361 12362 <div class="artikel-shine"></div> 12363 12364 <h1>EDUTOTO - Sensasi Relaksasi Premium Dengan Akses Nyaman & Fitur Lengkap</h1> 12365<p style="text-align: justify;"><a href="https://norita.mx/">EDUTOTO</a> menghadirkan ruang digital berkelas yang menonjolkan suasana relaksasi. Setiap detail disusun supaya pengguna langsung merasa betah, mulai dari akses yang ringkas hingga kumpulan fitur yang tersedia dalam satu platform.</p> 12366 12367<p style="text-align: justify;">Tampilan yang bersih dan kekinian membuat alur bermain terasa ringan. Pengguna bisa masuk ke berbagai permainan tanpa proses yang bertele-tele. Pemain baru maupun yang sudah rutin bermain tetap mudah menemukan menu yang diinginkan.</p> 12368 12369<p style="text-align: justify;">Sensasi relaksasi premium terbentuk dari visual yang anggun, katalog game pilihan, serta sistem yang terus disegarkan. Hasilnya, setiap sesi terasa tenang, stabil, dan tetap menyenangkan dari awal hingga akhir.</p> 12370 12371<p style="text-align: justify;">Akses EDUTOTO tersedia di komputer maupun ponsel. Layout yang menyesuaikan layar, navigasi yang mudah dipahami, dan transaksi yang ringan membuat login sampai bermain tetap praktis kapan pun dibutuhkan.</p> 12372 12373<p style="text-align: justify;">Jika Anda menginginkan tempat bermain yang lebih santai sekaligus terpercaya, EDUTOTO menjadi pilihan yang pas. Kombinasi akses cepat, layanan premium, dan reputasi baik memberi rasa nyaman sekaligus keyakinan di setiap kunjungan.</p> 12374</div> 12375 12376<style> 12377.artikel-glow-santaaa{ 12378 max-width: 820px; 12379 margin: 40px auto; 12380 padding: 36px; 12381 box-sizing: border-box; 12382 position: relative; 12383 overflow: hidden; 12384 border-radius: 30px; 12385 font-family: Arial, sans-serif; 12386 color: #ffffff; 12387 background: radial-gradient(circle at top left, rgba(234,179,8,.35), transparent 20%), radial-gradient(circle at bottom right, rgb(120,53,15), transparent 28%), linear-gradient(135deg, #EAB308, #8B6914, #FBBF24); 12388 border: 2px solid rgba(255, 255, 255, .55); 12389 box-shadow: 0 0 25px rgb(234 179 8), 0 0 55px rgb(251 191 36), 0 20px 45px rgba(0, 0, 0, .25), inset 0 0 35px rgba(139 105 20 / .45); 12390} 12391 12392.artikel-glow-santaaa:before{ 12393 content:""; 12394 position:absolute; 12395 inset:10px; 12396 border-radius:24px; 12397 border:1px solid rgba(255,255,255,.32); 12398 pointer-events:none; 12399} 12400 12401.artikel-shine{ 12402 position:absolute; 12403 top:-120px; 12404 left:-180px; 12405 width:280px; 12406 height:650px; 12407 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12408 transform:rotate(25deg); 12409 animation:artikelShine 4.5s infinite; 12410 pointer-events:none; 12411} 12412 12413@keyframes artikelShine{ 12414 0%{left:-220px;} 12415 55%{left:110%;} 12416 100%{left:110%;} 12417} 12418 12419.artikel-glow-santaaa h1, 12420.artikel-glow-santaaa h2{ 12421 position:relative; 12422 color:#000000; 12423 text-align:center; 12424 line-height:1.35; 12425 text-shadow: 12426 0 0 8px rgba(234 179 8 / 95%), 12427 0 0 18px rgb(251 191 36), 12428 0 2px 8px rgb(30 64 175); 12429} 12430 12431.artikel-glow-santaaa h1{ 12432 font-size:31px; 12433 margin:0 0 20px; 12434} 12435 12436.artikel-glow-santaaa h2{ 12437 font-size:24px; 12438 margin:28px 0 14px; 12439} 12440 12441.artikel-glow-santaaa p{ 12442 position:relative; 12443 margin:0 0 16px; 12444 color:#f8ffff; 12445 font-size:15px; 12446 line-height:1.85; 12447 text-align:justify; 12448 text-shadow:0 1px 4px rgba(0,0,0,.28); 12449} 12450 12451.artikel-glow-santaaa a{ 12452 color:#000000; 12453 font-weight:800; 12454 text-shadow:0 0 8px rgba(234,179,8,.75); 12455} 12456 12457.artikel-glow-santaaa img{ 12458 position:relative; 12459 display:block; 12460 max-width:100%; 12461 margin:16px auto 24px; 12462 border-radius:16px; 12463 border:1px solid rgba(255,255,255,.55); 12464 box-shadow: 12465 0 0 18px rgba(255,255,255,.55), 12466 0 12px 28px rgba(0,0,0,.28); 12467} 12468 12469.artikel-highlight{ 12470 position:relative; 12471 margin:24px 0; 12472 padding:22px; 12473 border-radius:22px;
12474 background:rgba(255,255,255,.20); 12475 border:1px solid rgba(255,255,255,.45); 12476 box-shadow: 12477 inset 0 1px 0 rgba(255,255,255,.65), 12478 inset 0 0 22px rgba(255,255,255,.14), 12479 0 10px 25px rgba(0,0,0,.18); 12480 backdrop-filter:blur(8px); 12481} 12482 12483.artikel-highlight:before{ 12484 content:""; 12485 position:absolute; 12486 left:0; 12487 right:0; 12488 top:0; 12489 height:45%; 12490 border-radius:22px 22px 0 0; 12491 background:linear-gradient(rgba(255,255,255,.35),rgba(255,255,255,.04)); 12492 pointer-events:none; 12493} 12494 12495.artikel-glow-santaaa ul{ 12496 position:relative; 12497 margin:15px 0 0 20px; 12498 padding:0; 12499} 12500 12501.artikel-glow-santaaa li{ 12502 margin-bottom:11px; 12503 padding:10px 12px; 12504 border-radius:12px; 12505 background:rgba(255,255,255,.16); 12506 color:#ffffff; 12507 line-height:1.6; 12508 box-shadow:inset 0 0 10px rgba(255,255,255,.08); 12509} 12510 12511@media(max-width:600px){ 12512 .artikel-glow-santaaa{ 12513 margin:25px 12px; 12514 padding:25px; 12515 border-radius:24px; 12516 } 12517 12518 .artikel-glow-santaaa h1{ 12519 font-size:24px; 12520 } 12521 12522 .artikel-glow-santaaa h2{ 12523 font-size:21px; 12524 } 12525} 12526</style> 12527 12528<div class="santaaa-faq-rainbow"> 12529 <div class="santaaa-shine"></div> 12530 12531 <div class="santaaa-faq-top"> 12532 <span>â PREMIUM FAQ â </span> 12533 <h2>Pertanyaan Umum EDUTOTO</h2> 12534 </div> 12535 12536 <div class="santaaa-rainbow-item"> 12537 <button type="button">Apa yang membuat pengalaman EDUTOTO terasa lebih premium?</button> 12538 <div class="santaaa-rainbow-answer"> 12539 <p>EDUTOTO memadukan desain elegan, akses yang ringkas, dan fitur lengkap dalam satu platform. Kombinasi itu membuat setiap sesi terasa lebih tenang, nyaman, dan berkelas sejak pengguna pertama kali masuk.</p> 12540 </div> 12541 </div> 12542 12543 <div class="santaaa-rainbow-item"> 12544 <button type="button">Apakah pemula bisa langsung bermain di EDUTOTO?</button> 12545 <div class="santaaa-rainbow-answer"> 12546 <p>Bisa. Antarmuka EDUTOTO sederhana dan modern, sehingga pemula tidak perlu melalui langkah yang berbelit. Menu favorit mudah ditemukan, dan alur bermain tetap jelas bagi pemain yang sudah aktif.</p> 12547 </div> 12548 </div> 12549 12550 <div class="santaaa-rainbow-item"> 12551 <button type="button">Bagaimana EDUTOTO menjaga suasana relaksasi saat bermain?</button> 12552 <div class="santaaa-rainbow-answer"> 12553 <p>Nuansa relaksasi dijaga lewat tampilan yang rapi, pilihan game berkualitas, dan sistem yang selalu diperbarui. Performa yang stabil membuat sesi bermain terasa tenang tanpa gangguan yang mengganggu kenyamanan.</p> 12554 </div> 12555 </div> 12556 12557 <div class="santaaa-rainbow-item"> 12558 <button type="button">Apakah EDUTOTO bisa dibuka di HP dan komputer?</button> 12559 <div class="santaaa-rainbow-answer"> 12560 <p>Ya. EDUTOTO mendukung akses di smartphone maupun komputer. Tampilan responsif, navigasi yang intuitif, dan proses transaksi yang ringan membuat login hingga bermain tetap praktis di perangkat mana pun.</p> 12561 </div> 12562 </div> 12563 12564 <div class="santaaa-rainbow-item"> 12565 <button type="button">Mengapa EDUTOTO cocok dijadikan pilihan utama?</button> 12566 <div class="santaaa-rainbow-answer"> 12567 <p>EDUTOTO cocok karena menawarkan akses cepat, fitur premium, dan reputasi positif. Bagi siapa pun yang mencari platform bermain yang lebih santai dan terpercaya, pengalaman di sini terasa lebih nyaman sekaligus meyakinkan.</p> 12568 </div> 12569 </div> 12570</div> 12571 12572<style> 12573.santaaa-faq-rainbow{ 12574 max-width: 760px; 12575 margin: 40px auto; 12576 padding: 30px; 12577 box-sizing: border-box; 12578 position: relative; 12579 overflow: hidden; 12580 border-radius: 32px; 12581 font-family: Arial, sans-serif; 12582 background: radial-gradient(circle at 15% 10%, rgba(234,179,8,.35), transparent 18%), radial-gradient(circle at 85% 15%, rgb(251,191,36), transparent 25%), radial-gradient(circle at 20% 90%, rgb(234,179,8), transparent 25%), linear-gradient(135deg, #EAB308, #8B6914, #78350F, #FBBF24); 12583 border: 2px solid rgb(234,179,8); 12584 box-shadow: 0 0 25px rgb(234,179,8), 0 0 55px rgba(251,191,36,.7), inset 0 0 35px rgba(139,105,20,.45); 12585} 12586 12587.santaaa-shine{ 12588 position:absolute; 12589 top:-90px; 12590 left:-120px; 12591 width:260px; 12592 height:520px; 12593 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12594 transform:rotate(25deg); 12595 animation:KTShine 4s infinite; 12596 pointer-events:none; 12597} 12598 12599@keyframes KTShine{ 12600 0%{left:-180px;} 12601 55%{left:110%;} 12602 100%{left:110%;} 12603} 12604 12605.santaaa-faq-top{ 12606 position:relative; 12607 text-align:center; 12608 margin-bottom:24px; 12609} 12610 12611.santaaa-faq-top span{ 12612 display:inline-block; 12613 padding:8px 22px; 12614 border-radius:999px; 12615 background:rgba(234,179,8,0); 12616 color:#dbdbdb; 12617 font-size:12px; 12618 font-weight:900; 12619 letter-spacing:2px; 12620 border:1px solid rgba(215,215,215,.62); 12621 box-shadow:inset 0 0 14px rgba(255,255,255,.25); 12622} 12623 12624.santaaa-faq-top h2{ 12625 margin:16px 0 8px; 12626 color:#000000; 12627 font-size:34px; 12628 line-height:1.15; 12629 text-shadow: 12630 0 0 10px rgb(234,179,8), 12631 0 0 22px rgb(251,191,36); 12632} 12633 12634.santaaa-faq-top p{ 12635 margin:0; 12636 color:#f3ffff; 12637 font-size:14px; 12638} 12639 12640.santaaa-rainbow-item{ 12641 position:relative; 12642 overflow:hidden; 12643 margin-bottom:14px; 12644 border-radius:20px; 12645 background:rgba(255,255,255,.20); 12646 border:1px solid rgba(255,255,255,.48); 12647 box-shadow: 12648 inset 0 1px 0 rgba(255,255,255,.6), 12649 0 10px 25px rgba(0,0,0,.18); 12650 backdrop-filter:blur(10px); 12651} 12652 12653.santaaa-rainbow-item:before{ 12654 content:""; 12655 position:absolute; 12656 top:0; 12657 left:0; 12658 right:0; 12659 height:45%; 12660 background:linear-gradient(rgba(255,255,255,.32),rgba(255,255,255,.04)); 12661 pointer-events:none; 12662} 12663 12664.santaaa-rainbow-item button{ 12665 width:100%; 12666 padding:18px 58px 18px 20px; 12667 border:0; 12668 background:#8B6914; 12669 color:#dbdbdb; 12670 text-align:left; 12671 cursor:pointer; 12672 font-size:15px; 12673 font-weight:900; 12674 position:relative; 12675 z-index:2; 12676 text-shadow:0 2px 6px rgba(0,0,0,.35); 12677} 12678 12679.santaaa-rainbow-item button:after{ 12680 content:"+"; 12681 position:absolute; 12682 right:18px; 12683 top:50%; 12684 transform:translateY(-50%); 12685 width:34px; 12686 height:34px; 12687 border-radius:50%; 12688 background:linear-gradient(135deg,#EAB308,#8B6914); 12689 color:#d1d1d1; 12690 text-align:center; 12691 line-height:34px; 12692 font-size:24px; 12693 font-weight:900; 12694 box-shadow: 12695 0 0 14px rgba(255,255,255,.8), 12696 inset 0 -4px 8px rgba(0,0,0,.18); 12697} 12698 12699.santaaa-rainbow-item.active button:after{ 12700 content:"â"; 12701 background:linear-gradient(135deg,#FBBF24,#78350F); 12702} 12703 12704.santaaa-rainbow-answer{ 12705 max-height:0; 12706 overflow:hidden; 12707 transition:max-height .35s ease; 12708 position:relative; 12709 z-index:2; 12710} 12711 12712.santaaa-rainbow-item.active .santaaa-rainbow-answer{ 12713 max-height:170px; 12714} 12715 12716.santaaa-rainbow-answer p{ 12717 margin:0; 12718 padding:0 20px 18px; 12719 color:#dbdbdb; 12720 font-size:14px; 12721 line-height:1.7; 12722 text-shadow:0 2px 5px rgba(0,0,0); 12723} 12724 12725@media(max-width:600px){ 12726 .santaaa-faq-rainbow{ 12727 margin:25px 12px; 12728 padding:22px; 12729 border-radius:24px; 12730 } 12731 12732 .santaaa-faq-top h2{ 12733 font-size:26px; 12734 } 12735} 12736</style> 12737
12738<script> 12739document.querySelectorAll(".santaaa-rainbow-item button").forEach(function(btn){ 12740 btn.addEventListener("click", function(){ 12741 this.parentElement.classList.toggle("active"); 12742 }); 12743}); 12744</script>
12744 12745<style> 12746@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap'); 12747 12748.cyber-mecha { 12749 max-width: 800px; 12750 margin: 40px auto; 12751 12752 /* deep luxury red */ 12753 background: radial-gradient(circle at top, #14b1d7, #14b1d7 70%); 12754 12755 border-radius: 16px; 12756 border: 1px solid rgba(255, 90, 90, 0.35); 12757 12758 box-shadow: 12759 0 0 25px rgba(140, 30, 30, 0.35), 12760 inset 0 0 20px rgba(255, 60, 60, 0.10); 12761 12762 padding: 25px; 12763 font-family: 'Orbitron', sans-serif; 12764 color: #f7e6e6; 12765 position: relative; 12766 overflow: hidden; 12767} 12768 12769/* scanline overlay */ 12770.cyber-mecha::before { 12771 content: ""; 12772 position: absolute; 12773 inset: 0; 12774 background: repeating-linear-gradient( 12775 to bottom, 12776 rgba(255,90,90,0.03), 12777 rgba(255,90,90,0.03) 1px, 12778 transparent 1px, 12779 transparent 3px 12780 ); 12781 pointer-events: none; 12782} 12783 12784.pd-full-desc-title { 12785 text-align: center; 12786 font-size: 22px; 12787 12788 /* elegant muted red */ 12789 color: #e08b8b; 12790 12791 letter-spacing: 2px; 12792 text-shadow: 0 0 10px rgba(224, 139, 139, 0.4); 12793 margin-bottom: 20px; 12794} 12795 12796.mecha-header h3 { 12797 text-align: center; 12798 font-size: 18px; 12799 color: #ffb3b3; 12800 text-shadow: 0 0 8px rgba(255, 90, 90, 0.5); 12801 margin-bottom: 15px; 12802} 12803 12804.mecha-image img { 12805 width: 100%; 12806 border-radius: 10px; 12807 border: 1px solid rgba(255, 90, 90, 0.4); 12808 box-shadow: 0 0 20px rgba(255, 60, 60, 0.25); 12809 margin-bottom: 20px; 12810} 12811 12812.mecha-content p { 12813 font-size: 14px; 12814 line-height: 1.8; 12815 margin-bottom: 14px; 12816 color: #f3dede; 12817} 12818 12819.glow { 12820 color: #e08b8b; 12821 text-shadow: 0 0 6px rgba(224, 139, 139, 0.5); 12822 font-weight: 600; 12823} 12824 12825.glow-strong { 12826 color: #ff6b6b; 12827 text-shadow: 12828 0 0 10px rgba(255, 90, 90, 0.6), 12829 0 0 20px rgba(255, 60, 60, 0.4); 12830 font-weight: 700; 12831} 12832 12833.final-line { 12834 margin-top: 18px; 12835 padding: 10px; 12836 border: 1px solid rgba(255, 90, 90, 0.35); 12837 background: rgba(60, 0, 0, 0.25); 12838 border-radius: 8px; 12839 text-align: center; 12840 color: #ffd0d0; 12841} 12842</style> 12843 12844 12845 12846<section class="pd-related-full"> 12847 <h2 class="pd-related-title">Provider Game Populer</h2> 12848 <div class="pd-related-grid pd-related-grid--full"> 12849 12850 <a class="pd-related-card" href="https://norita.mx/"> 12851 <div class="thumb"> 12852 <img src="img/banner-edutoto.webp" alt="Pragmatic Play"> 12853 <span class="mini-disc">185+</span> 12854 </div> 12855 <div class="info"> 12856 <h3>Pragmatic Play</h3> 12857 <div class="spec-chips">Game Slot <span class="spec-divider">|</span> Populer <span class="spec-divider">|</span> Update Rutin <span class="spec-divider">|</span> Favorit</div> 12858 <span class="price"> 12859 <span class="final">385 Game</span> 12860 <span class="ori">402 Judul</span> 12861 </span> 12862 <span class="sold">Dimainkan 18.246+</span> 12863 <span class="stock-dot low" title="Populer"></span> 12864 </div> 12865 </a> 12866 12867 <a class="pd-related-card" href="https://norita.mx/"> 12868 <div class="thumb"> 12869 <img src="img/banner-edutoto.webp" alt="PG Soft"> 12870 <span class="mini-disc">142+</span> 12871 </div> 12872 <div class="info"> 12873 <h3>PG Soft</h3> 12874 <div class="spec-chips">Mobile Friendly <span class="spec-divider">|</span> Game Baru <span class="spec-divider">|</span> Populer <span class="spec-divider">|</span> Favorit</div> 12875 <span class="price"> 12876 <span class="final">214 Game</span> 12877 <span class="ori">228 Judul</span> 12878 </span> 12879 <span class="sold">Dimainkan 15.913+</span> 12880 <span class="stock-dot ready" title="Tersedia"></span> 12881 </div> 12882 </a> 12883 12884 <a class="pd-related-card" href="https://norita.mx/"> 12885 <div class="thumb"> 12886 <img src="img/banner-edutoto.webp" alt="Habanero"> 12887 <span class="mini-disc">96+</span> 12888 </div> 12889 <div class="info"> 12890 <h3>Habanero</h3> 12891 <div class="spec-chips">EDUTOTO DAFTAR <span class="spec-divider">|</span> Koleksi Lengkap <span class="spec-divider">|</span> Terbaru <span class="spec-divider">|</span> Pilihan</div>
12892 <span class="price"> 12893 <span class="final">187 Game</span> 12894 <span class="ori">195 Judul</span> 12895 </span> 12896 <span class="sold">Dimainkan 12.774+</span> 12897 <span class="stock-dot ready" title="Tersedia"></span> 12898 </div> 12899 </a> 12900 12901 <a class="pd-related-card" href="https://norita.mx/"> 12902 <div class="thumb"> 12903 <img src="img/banner-edutoto.webp" alt="Microgaming"> 12904 <span class="mini-disc">210+</span> 12905 </div> 12906 <div class="info"> 12907 <h3>Microgaming</h3> 12908 <div class="spec-chips">Game Klasik <span class="spec-divider">|</span> Provider Populer <span class="spec-divider">|</span> Terlengkap <span class="spec-divider">|</span> Update</div> 12909 <span class="price"> 12910 <span class="final">356 Game</span> 12911 <span class="ori">369 Judul</span> 12912 </span> 12913 <span class="sold">Dimainkan 10.638+</span> 12914 <span class="stock-dot ready" title="Tersedia"></span> 12915 </div> 12916 </a> 12917 12918 <a class="pd-related-card" href="https://norita.mx/"> 12919 <div class="thumb"> 12920 <img src="img/banner-edutoto.webp" alt="Spadegaming"> 12921 <span class="mini-disc">118+</span> 12922 </div> 12923 <div class="info"> 12924 <h3>Spadegaming</h3> 12925 <div class="spec-chips">Game Favorit <span class="spec-divider">|</span> Provider Terbaik <span class="spec-divider">|</span> Populer <span class="spec-divider">|</span> Mudah Diakses</div> 12926 <span class="price"> 12927 <span class="final">168 Game</span> 12928 <span class="ori">179 Judul</span> 12929 </span> 12930 <span class="sold">Dimainkan 9.427+</span> 12931 <span class="stock-dot low" title="Populer"></span> 12932 </div> 12933 </a> 12934 12935 <a class="pd-related-card" href="https://norita.mx/"> 12936 <div class="thumb"> 12937 <img src="img/banner-edutoto.webp" alt="CQ9 Gaming"> 12938 <span class="mini-disc">134+</span> 12939 </div> 12940 <div class="info"> 12941 <h3>CQ9 Gaming</h3> 12942 <div class="spec-chips">Game Terbaru <span class="spec-divider">|</span> Provider Pilihan <span class="spec-divider">|</span> Koleksi Lengkap <span class="spec-divider">|</span> Populer</div> 12943 <span class="price"> 12944 <span class="final">196 Game</span> 12945 <span class="ori">207 Judul</span> 12946 </span> 12947 <span class="sold">Dimainkan 11.385+</span> 12948 <span class="stock-dot ready" title="Tersedia"></span> 12949 </div> 12950 </a> 12951 12952 </div> 12953</section> 12954 12955 <div class="pd-mobile-fab" id="pd-mobile-fab" data-pid="325" data-price="20789000" data-voucher-code="" data-voucher-value="0"><div class="mf-actions"><button type="button" class="mf-btn mf-share" id="mf-share" aria-label="Bagikan"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></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">
12955<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> 12956 12957 <button type="button" 12958 class="pd-scroll-top" 12959 id="pd-scroll-top" 12960 aria-label="Kembali ke atas" 12961 title="Kembali ke atas"> 12962 <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"> 12963 <path fill="currentColor" d="M7.41 14.59 12 10l4.59 4.59L18 13.17l-6-6-6 6z"/> 12964 </svg> 12965 </button> 12966</main> 12967 12968<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="img/banner-edutoto.webp" alt="EDUTOTO" class="pd-share-preview-img"><div class="pd-share-preview-info"><h3 class="pd-share-preview-name">EDUTOTO</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"><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">
12968<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://norita.mx/" 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> 12969 12970<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:#EAB308;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 #EAB308}.toast.warn{border-left:4px solid #CA8A04}.toast .progress{position:absolute;left:0;bottom:0;height:3px;background:linear-gradient(90deg,#FBBF24,#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> 12971<style> 12972.toast .msg { word-break: break-word; overflow-wrap: anywhere; } 12973 12974.toast-root { bottom: max(12px, env(safe-area-inset-bottom, 12px)); } 12975 12976@media (max-width: 540px) { 12977 .toast-root { 12978 left: 0; right: 0; transform: none; 12979 padding: 0 12px; 12980 bottom: calc(max(12px, env(safe-area-inset-bottom, 12px)) + var(--fab-offset, 72px)); 12981 max-width: none; 12982 } 12983 .toast { 12984 grid-template-columns: 1fr; /* 1 kolom */ 12985 position: relative; 12986 padding: 14px 14px 12px 14px; 12987 border-radius: 16px; 12988 } 12989 .toast .ico { 12990 position: absolute; left: 14px; top: 14px; 12991 margin: 0; transform: translateY(1px); 12992 } 12993 .toast .cnt { padding-left: 34px; } 12994 .toast .ttl { font-size: 15px; margin-bottom: 6px; } 12995 .toast .msg { font-size: 13px; opacity: .96; } 12996 .toast .actions { 12997 margin-top: 12px; gap: 10px; width: 100%; 12998 display: grid; grid-template-columns: 1fr 1fr; 12999 } 13000 .toast .btn { width: 100%; min-width: 0; padding: 10px 0; } 13001 .toast .close { position: absolute; right: 6px; top: 6px; } 13002} 13003</style> 13004
13005<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13006(function adjustToastFabOffset(){ 13007 const root = document.getElementById('toastRoot'); 13008 if (!root) return; 13009 13010 function isVisible(el){ 13011 if (!el) return false; 13012 const cs = getComputedStyle(el); 13013 if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; 13014 const r = el.getBoundingClientRect(); 13015 return r.width > 0 && r.height > 0; 13016 } 13017 13018 function update(){ 13019 const fab = document.getElementById('pd-mobile-fab'); 13020 let extra = 16; 13021 if (isVisible(fab)) { 13022 const h = Math.round(fab.getBoundingClientRect().height) || 56; 13023 extra = h + 16; // FAB height + margin 13024 } 13025 root.style.setProperty('--fab-offset', extra + 'px'); 13026 } 13027 13028 window.addEventListener('resize', update, { passive: true }); 13029 const fab = document.getElementById('pd-mobile-fab'); 13030 if (window.ResizeObserver && fab) { 13031 const ro = new ResizeObserver(update); 13032 ro.observe(fab); 13033 } 13034 13035 update(); 13036})(); 13037</script>
13037 13038<div class="toast-root" id="toastRoot" aria-live="polite" aria-atomic="true"></div> 13039
13040<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="#CA8A04"><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="#EAB308"><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="#EAB308"/></svg>'} 13041window.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>`; 13042 13043 const close = ()=> { 13044 if (!el.parentNode) return; 13045 el.style.transition = 'transform .2s ease, opacity .2s ease'; 13046 el.style.opacity = '0'; el.style.transform = 'translateY(10px)'; 13047 setTimeout(()=>{ if (el.parentNode) el.parentNode.removeChild(el); }, 220); 13048 }; 13049 13050 el.querySelector('.close').addEventListener('click', close); 13051 el.querySelectorAll('.actions .btn').forEach(btn=>{ 13052 btn.addEventListener('click', ()=>{ 13053 const lab = btn.getAttribute('data-act'); 13054 try { 13055 (o.actions||[]).forEach(a=>{ if (a.label===lab && typeof a.onClick==='function') a.onClick(); }); 13056 } catch(_){} 13057 close(); 13058 }); 13059 }); 13060 13061 root.appendChild(el); 13062 setTimeout(close, o.duration); 13063 return el; 13064 }; 13065})(); 13066 13067const _0x1a2b=[{"url":"img/banner-edutoto.webp","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":"img/banner-edutoto.webp","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"img/banner-edutoto.webp"},{"url":"img/banner-edutoto.webp","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":"img/banner-edutoto.webp","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":"img/banner-edutoto.webp","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":"img/banner-edutoto.webp","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-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=>
13067c.textContent.trim()).filter(Boolean);return b.length?b.join(' / '):''} 13068function encodeVariantHash(id){if(!id||id<=0)return '';const xor=id^0x4A5D;return xor.toString(36)} 13069function decodeVariantHash(hash){if(!hash)return 0;const xor=parseInt(hash,36);return isNaN(xor)?0:xor^0x4A5D} 13070 13071function 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})} 13072 13073function executeBuyNowLegacy(id,variantId){const a=getVariantSummaryText(),b=(function(){const c=document.getElementById('pd-sku');return c?c.textContent.trim():_0x5e6f})(); 13074 13075 let willRedirect = false; 13076 try { 13077 if (window.addToCart) { 13078 window.addToCart(id, 1, variantId); 13079 willRedirect = false; 13080 } 13081 } catch(_){} 13082 13083 if (willRedirect) { 13084 showToast({ 13085 type: 'info', 13086 title: 'Menuju Keranjang', 13087 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13088 duration: 1800 13089 }); 13090 setTimeout(()=>{ window.location.href='/keranjang'; }, 1200); 13091 } else { 13092 showToast({ 13093 type: 'warn', 13094 title: 'Fitur Beli Langsung Segera Hadir', 13095 message: `Sementara ini Anda bisa menambahkan produk ke keranjang terlebih dahulu.<br><strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}`, 13096 duration: 6000, 13097 actions: [ 13098 { label: '+ Keranjang', onClick: ()=> { try{ if (window.addToCart) window.addToCart(id,1,variantId); }catch(_){} } }, 13099 { label: 'Hubungi CS', variant: 'alt', onClick: ()=> { window.open('https://wa.me/6281380009898','_blank'); } } 13100 ] 13101 }); 13102 } 13103} 13104 13105document.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)})}}); 13106 13107function _0x2m3n(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} 13108 13109(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')}})})(); 13110 13111(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;
13111slides.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})})(); 13112 13113/* ================= Simulasi Cicilan ================= */ 13114(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'} 13115 13116function _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'} 13117 13118card.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)}); 13119 13120window.instSetBasePrice=function(newPrice){setBasePrice(newPrice);_0x4o5p(activeBank())};_0x4o5p(activeBank()); 13121})(); 13122 13123/* ====== Fokus gambar varian ====== */ 13124function _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 ''}}} 13125function _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}} 13126 if (c){ 13127 const targetName = _0x8s9t(url); 13128 const slideImg = Array.from(c.querySelectorAll('img')).find(im => _0x8s9t(im.src) === targetName); 13129 if (slideImg){ 13130 13131 try { 13132 const container = c; 13133 const imgRect = slideImg.getBoundingClientRect(); 13134 const contRect = container.getBoundingClientRect(); 13135 const offsetLeft = slideImg.offsetLeft - container.offsetLeft; 13136 const center = offsetLeft - (container.clientWidth - slideImg.clientWidth) / 2; 13137 container.scrollTo({ left: Math.max(0, center), behavior: 'smooth' }); 13138 } catch(_){ 13139 13140 slideImg.scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); 13141 } 13142 } 13143 } 13144} 13145function findImageForVariant(variant){ 13146 if (!variant || !variant.attrs) return null; 13147 const tokens = []; 13148 try{ 13149 Object.values(variant.attrs).forEach(v=>{ 13150 if (!v) return; 13151 const raw = String(v); 13152 tokens.push(raw); 13153 tokens.push(normalize(raw)); 13154 }); 13155 }catch(_){} 13156 if (variant.sku){ 13157 tokens.push(String(variant.sku)); 13158 tokens.push(normalize(String(variant.sku))); 13159 } 13160 if (variant.image_url) { 13161 tokens.push(String(variant.image_url)); 13162 } 13163 let best = null; 13164 let bestScore = -1; 13165 13166 for (const g of (galleryData||[])){ 13167 const slugN = normalize(g.slug||''); 13168 const altN = normalize(g.alt||''); 13169 const file = _0x8s9t(g.url||''); 13170 const fileN = normalize(file); 13171 const imgU = g.url; 13172 13173 for (const t of tokens){ 13174 const tN = normalize(t); 13175 if (!tN) continue; 13176 13177 let score = -1; 13178 if (imgU === tN) score = 300; 13179 if (slugN && slugN === tN) score = 300; 13180 else if (fileN && fileN.includes(tN)) score = 200; 13181 else if (altN && altN.includes(tN)) score = 120; 13182 13183 if (score > bestScore){ 13184 bestScore = score; 13185 best = g; 13186 } 13187 if (bestScore >= 300) break; 13188 } 13189 if (bestScore >= 300) break; 13190 } 13191 return bestScore >= 120 ? best : null; 13192} 13193function _0xey3z(variant){const a=_0xcw2x(variant);if(a&&a.url){_0xau1v(a.url,a.alt||'')}} 13194 13195/* ====== Layout mobile ====== */ 13196(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; 13197 13198 let pricePlaced = false;
13199 let varPlaced = false; 13200 let titlePlaced = false; 13201 let promoPlaced = false; 13202 const mq = window.matchMedia('(max-width: 640px)'); 13203 13204 function apply(){ 13205 if (mq.matches) { 13206 if (!titlePlaced && a && titleRow && d.contains(titleRow)) { try{a.appendChild(titleRow);titlePlaced=true;}catch(_){} } 13207 if (!pricePlaced && a && b) { try{a.appendChild(b);pricePlaced=true;}catch(_){} } 13208 if (!varPlaced && f && d && e) { 13209 try{ 13210 f.hidden = false; 13211 if (i && !f.contains(i)) f.appendChild(i); 13212 if (g && !f.contains(g)) f.appendChild(g); 13213 if(d.contains(e))d.insertBefore(f, e); 13214 varPlaced = true; 13215 }catch(_){} 13216 } 13217 if (!promoPlaced && promoBanner && compareSection && compareSection.parentNode) { 13218 try{ 13219 compareSection.parentNode.insertBefore(promoBanner, compareSection); 13220 promoPlaced = true; 13221 }catch(_){} 13222 } 13223 } else { 13224 if (titlePlaced && titleRow && d) { 13225 try{ 13226 const vcombined = document.getElementById('pd-variant-combined'); 13227 if (vcombined && d.contains(vcombined)) { 13228 d.insertBefore(titleRow, vcombined); 13229 } else { 13230 d.insertBefore(titleRow, d.firstChild); 13231 } 13232 titlePlaced=false; 13233 }catch(_){} 13234 } 13235 if (pricePlaced && b && c && c.parentNode) { 13236 try{c.parentNode.insertBefore(b, c.nextSibling);pricePlaced=false;}catch(_){} 13237 } 13238 if (varPlaced) { 13239 try{ 13240 if (i && j && j.parentNode && !j.parentNode.contains(i)) j.parentNode.insertBefore(i, j.nextSibling); 13241 if (g && h && h.parentNode && !h.parentNode.contains(g)) h.parentNode.insertBefore(g, h.nextSibling); 13242 if (f) { f.hidden = true; f.innerHTML = ''; } 13243 varPlaced = false; 13244 }catch(_){} 13245 } 13246 if (promoPlaced && promoBanner && b) { 13247 try{ 13248 b.appendChild(promoBanner); 13249 promoPlaced = false; 13250 }catch(_){} 13251 } 13252 } 13253 } 13254 13255 mq.addEventListener ? mq.addEventListener('change', apply) : mq.addListener(apply); 13256 apply(); 13257 13258 const fab = document.getElementById('pd-mobile-fab'); 13259 if (fab) { 13260 const pid = parseInt(fab.getAttribute('data-pid')||'0',10) || 325; 13261 const btnCart = document.getElementById('mf-cart'); 13262 13263 if (btnCart) btnCart.addEventListener('click', function(){ 13264 const varInput = document.getElementById('pd-variant-id'); 13265 const variantId = varInput ? parseInt(varInput.value||'0',10) : 0; 13266 try { if (window.addToCart) addToCart(pid, 1, variantId); } catch(_){} 13267 const variantText = getVariantSummaryText(); 13268 const skuText = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim():PD_SKU; })(); 13269 showToast({ 13270 type: 'success', 13271 title: 'Berhasil ditambahkan ke Keranjang', 13272 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13273 duration: 5000, 13274 actions: [ 13275 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 13276 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 13277 ] 13278 }); 13279 }); 13280 } 13281})(); 13282 13283/* ====== Auto-hide installment banner if sidebar too tall (Desktop only) ====== */ 13284(function(){ 13285 function adjustSidebarBanner() { 13286 if (window.innerWidth <= 768) return; 13287 const sidebar = document.querySelector('.pd-sidebar'); 13288 const infoCard = document.querySelector('.pd-info-card'); 13289 const banner = document.querySelector('.pd-side-card.inst-alt-banner.inst-alt-banner--compact'); 13290 if (!sidebar || !infoCard || !banner) return; 13291 const sidebarHeight = sidebar.offsetHeight; 13292 const infoCardHeight = infoCard.offsetHeight; 13293 if (sidebarHeight > infoCardHeight) { 13294 banner.style.display = 'none'; 13295 } else { 13296 banner.style.display = ''; 13297 } 13298 } 13299 if (document.readyState === 'loading') { 13300 document.addEventListener('DOMContentLoaded', adjustSidebarBanner); 13301 } else { 13302 adjustSidebarBanner(); 13303 } 13304 let resizeTimer; 13305 window.addEventListener('resize', function() { 13306 clearTimeout(resizeTimer); 13307 resizeTimer = setTimeout(adjustSidebarBanner, 250); 13308 });
13309 const pwpSection = document.getElementById('pwp-deals-section'); 13310 if (pwpSection) { 13311 const observer = new MutationObserver(function(mutations) { 13312 let shouldRecheck = false; 13313 mutations.forEach(function(mutation) { 13314 if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') { 13315 shouldRecheck = true; 13316 } else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 13317 shouldRecheck = true; 13318 } 13319 }); 13320 if (shouldRecheck) { 13321 setTimeout(adjustSidebarBanner, 100); 13322 } 13323 }); 13324 observer.observe(pwpSection, { 13325 attributes: true, 13326 childList: true, 13327 subtree: true 13328 }); 13329 } 13330 window.adjustSidebarBanner = adjustSidebarBanner; 13331})(); 13332 13333/* ====== Global constants ====== */ 13334const IS_DIGITAL=false; 13335const IS_PREORDER=false; 13336 13337/* ====== Variants state (REVISED) ====== */ 13338(function initVariants(){ 13339const _0x7g8h=[];window.variants=_0x7g8h; 13340window.isPreorderProduct=false; 13341 const attrOrder = []; 13342 13343 if (!variants.length || !attrOrder.length) return; 13344 13345 function comboKey(sel){ 13346 return attrOrder.map(a => a + '=' + (sel[a] ?? '')).join('|'); 13347 } 13348 13349 const variantMap = {};
13350 variants.forEach(v=>{ 13351 const key = comboKey(v.attrs); 13352 variantMap[key] = v; 13353 }); 13354 13355 function readSelection(){ 13356 const sel = {}; 13357 attrOrder.forEach(a=>{ 13358 const r = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio]:checked`); 13359 sel[a] = r ? r.value : ''; 13360 }); 13361 return sel; 13362 } 13363 13364 function findVariant(sel){ 13365 return variantMap[comboKey(sel)] || null; 13366 } 13367 13368 function autoFillMissing(sel){ 13369 let changed = false; 13370 for (const a of attrOrder) { 13371 if (sel[a]) continue; 13372 const possible = []; 13373 const seen = new Set(); 13374 variants.forEach(v=>{ 13375 let ok = true; 13376 for (const a2 of attrOrder) { 13377 if (a2 === a) continue; 13378 if (sel[a2] && v.attrs[a2] !== sel[a2]) { ok = false; break; } 13379 } 13380 if (ok) { 13381 const val = v.attrs[a]; 13382 if (!seen.has(val)) { seen.add(val); possible.push(val); } 13383 } 13384 }); 13385 if (possible.length === 1) { 13386 const val = possible[0]; 13387 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13388 if (input && !input.checked) { input.checked = true; sel[a] = val; changed = true; } 13389 } 13390 } 13391 return changed; 13392 } 13393 13394 function rupiah(n){ 13395 n = parseInt(n||0,10)||0; 13396 return 'Rp ' + n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.'); 13397 } 13398 13399 function focusGalleryByUrl(imageUrl) { 13400 const isMobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; 13401 if (isMobile) { 13402 _0xau1v(imageUrl); 13403 return; 13404 } 13405 13406 const gallery = document.getElementById('pd-thumb-gallery'); 13407 const targetName = _0x8s9t(imageUrl); 13408 13409 if (gallery) { 13410 const btns = Array.from(gallery.querySelectorAll('.pd-thumb-btn')); 13411 const btn = btns.find(b => _0x8s9t(b.getAttribute('data-full') || '') === targetName); 13412 if (btn) { btn.click(); return; } 13413 } 13414 13415 _0xau1v(imageUrl); 13416 } 13417 13418 13419function formatStockLabel(n){n=parseInt(n||0,10)||0;return(n>10)?'10+':String(n)} 13420function 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(_){}} 13421 13422 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)} 13423 13424 function applyVariant(v, focusImage = true){ 13425 const elCur = document.getElementById('pd-price-current'); 13426 const elOri = document.getElementById('pd-price-original'); 13427 const elDisc = document.getElementById('pd-discount-badge'); 13428 const elSku = document.getElementById('pd-sku'); 13429 const elStock= document.getElementById('pd-stock-badge'); 13430 const elQty = document.getElementById('qty'); 13431 const btnAdd = document.getElementById('btn-add-cart'); 13432 const btnBuy = document.getElementById('btn-buy'); 13433 const hidVar = document.getElementById('pd-variant-id'); 13434 13435 const mfCart= document.getElementById('mf-cart'); 13436 const mfBuy = document.getElementById('mf-buy'); 13437 const fab = document.getElementById('pd-mobile-fab'); 13438 13439 if (hidVar) hidVar.value = v ? v.id : 0; 13440 if (elCur) elCur.textContent = rupiah(v ? v.final : 0); 13441 if (elSku && v && v.sku) elSku.textContent = v.sku; 13442 13443 if (v && v.ori > v.final) { 13444 if (elOri) { elOri.style.display='inline'; elOri.textContent = rupiah(v.ori); } 13445 if (elDisc) { elDisc.style.display='inline'; elDisc.textContent = '-' + (v.disc||0) + '%'; } 13446 } else { 13447 if (elOri) elOri.style.display='none'; 13448 if (elDisc) elDisc.style.display='none'; 13449 } 13450 13451 const stock = v ? (parseInt(v.stock||0,10)||0) : 0; 13452 let effectiveStock = stock; 13453 let isActivePreorder = false; 13454 if (window.isPreorderProduct && v && 'preorder_remaining' in v && v.preorder_remaining !== null) { 13455 effectiveStock = parseInt(v.preorder_remaining, 10); 13456 if (isNaN(effectiveStock)) effectiveStock = 0; 13457 isActivePreorder = true; 13458 } 13459 if (elStock){ 13460 elStock.classList.remove('in','out'); 13461 if (effectiveStock>0){ 13462 elStock.classList.add('in'); 13463 elStock.textContent = IS_DIGITAL ? 'Tersedia' : (isActivePreorder ? 'Sisa Slot: ' + effectiveStock : 'Stok: ' + formatStockLabel(effectiveStock)); 13464 } else { 13465 elStock.classList.add('out'); 13466 elStock.textContent = 'Habis'; 13467 } 13468 } 13469 if (elQty){ 13470 elQty.max = String(effectiveStock>0?effectiveStock:0); 13471 if (effectiveStock<=0) { elQty.value = '0'; elQty.disabled = true; } 13472 else { if (parseInt(elQty.value||'1',10) < 1) elQty.value='1'; elQty.disabled=false; } 13473 } 13474 13475 const shouldDisable = !(v && effectiveStock>0); 13476 const updateButtons = () => { 13477 const btnAddCart = document.getElementById('btn-add-cart'); 13478 const btnBuyNow = document.getElementById('btn-buy'); 13479 const mobileCart = document.getElementById('mf-cart'); 13480 const mobileBuy = document.getElementById('mf-buy'); 13481 [btnAddCart, btnBuyNow, mobileCart, mobileBuy].forEac
13481h(btn => { 13482 if (!btn) return; 13483 if (shouldDisable) { 13484 btn.disabled = true; 13485 btn.setAttribute('disabled', 'disabled'); 13486 btn.classList.add('stock-disabled'); 13487 btn.setAttribute('data-stock-blocked', 'true'); 13488 btn.setAttribute('data-stock-reason', window.isPreorderProduct ? 'Pre-order habis' : 'Stok habis'); 13489 } else { 13490 btn.disabled = false; 13491 btn.removeAttribute('disabled'); 13492 btn.classList.remove('stock-disabled'); 13493 btn.removeAttribute('data-stock-blocked'); 13494 btn.removeAttribute('data-stock-reason'); 13495 } 13496 }); 13497 }; 13498 updateButtons(); 13499 setTimeout(updateButtons, 10); 13500 setTimeout(updateButtons, 50); 13501 setTimeout(updateButtons, 100); 13502 if (mfBuy && v) { 13503 const voucherBtn = document.getElementById('btn-buy-voucher'); 13504 const voucherCode = fab ? fab.dataset.voucherCode : ''; 13505 let voucherDiscount = 0; 13506 13507 if (voucherBtn && voucherCode) { 13508 const vType = voucherBtn.dataset.voucherType; 13509 const vValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 13510 const vMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 13511 const finalPrice = v.final || 0; 13512 if (vType && vValue > 0) { 13513 voucherDiscount = calcVoucherDisc(vType, vValue, finalPrice, vMax); 13514 if (fab) fab.dataset.voucherValue = voucherDiscount.toString(); 13515 } 13516 } 13517 13518 const finalPrice = v.final || 0; 13519 const textEl = mfBuy.querySelector('.mf-buy-text'); 13520 const priceEl = mfBuy.querySelector('.mf-buy-price'); 13521 13522 if (effectiveStock > 0) { 13523 if (IS_PREORDER) { 13524 if (textEl) textEl.textContent = 'Pre Order Sekarang'; 13525 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13526 } else if (voucherCode && voucherDiscount > 0) { 13527 if (textEl) textEl.textContent = 'Beli dengan Voucher'; 13528 if (priceEl) priceEl.textContent = rupiah(Math.max(0, finalPrice - voucherDiscount)); 13529 } else { 13530 if (textEl) textEl.textContent = 'Beli Sekarang'; 13531 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13532 } 13533 } else { 13534 if (textEl) textEl.textContent = 'Stok Habis'; 13535 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13536 } 13537 } 13538 13539 const isMobile = window.initMobile ?? 0; 13540 if (window.instSetBasePrice) window.instSetBasePrice(v ? (isMobile == '1' ? v.ori : v.final) : 0); 13541 if (window.updateSoldBySku) window.updateSoldBySku(v.sku); 13542 updateBranchPopover(v); 13543 if (v && v.image_url && focusImage) focusGalleryByUrl(v.image_url); 13544 if (v) { 13545 window.dispatchEvent(new CustomEvent('pd-variant-changed', { 13546 detail: { 13547 price: v.final, 13548 originalPrice: v.ori, 13549 discount: v.disc, 13550 stock: effectiveStock, 13551 sku: v.sku, 13552 variant_id: v.id 13553 } 13554 })); 13555 13556 const encodedHash = encodeVariantHash(v.id); 13557 if (encodedHash && window.history && window.history.replaceState) { 13558 const newHash = '#v=' + encodedHash; 13559 const newUrl = window.location.pathname + window.location.search + newHash; 13560 window.history.replaceState(null, '', newUrl); 13561 } 13562 } 13563 } 13564 13565 let sel = readSelection(); 13566 if (autoFillMissing(sel)) sel = readSelection(); 13567 13568 let v = findVariant(sel); 13569 if (!v) { 13570 const candidates = variants.filter(variant=>{ 13571 return attrOrder.every(a=>{ 13572 if (!sel[a]) return true; 13573 return variant.attrs[a] === sel[a]; 13574 }); 13575 }); 13576 if (candidates.length >= 1) { 13577 v = candidates[0]; 13578 attrOrder.forEach(a=> sel[a] = v.attrs[a]); 13579 attrOrder.forEach(a=>{ 13580 const val = sel[a]; 13581 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13582 if (input) input.checked = true; 13583 }); 13584 } 13585 } 13586 13587 const initSel = readSelection(); 13588 const initVar = findVariant(initSel) || variants[0] || null; 13589 if (document.readyState === 'loading') { 13590 document.addEventListener('DOMContentLoaded', () => applyVariant(initVar, false)); 13591 } else { 13592 applyVariant(initVar, false); 13593 } 13594 13595function updateSummary(sel){Object.entries(sel).forEach(([a,val])=>{const el=document.getElementById('vs-'+a);if(el)el.textContent=val||'-'})} 13596 function refreshOptions(sel){ 13597 attrOrder.forEach(a=>{ 13598 const group = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"]`); 13599 if (!group) return; 13600 const chips = group.querySelectorAll('.v-chip'); 13601 chips.forEach(ch=>{ 13602 const val = ch.getAttribute('data-value'); 13603 const possible = variants.some(v=>{ 13604 if (v.attrs[a] !== val) return false; 13605 for (const a2 of attrOrder) { 13606 if (a2 === a) continue; 13607 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13608 } 13609 return true; 13610 }); 13611 const input = ch.querySelector('input[type=radio]'); 13612 const disabled = !possible; 13613 if (input) input.disabled = disabled; 13614 ch.classList.toggle('disabled', disabled); 13615 13616 let candidateVariant = null; 13617 if (possible) { 13618 candidateVariant = variants.find(v=>{ 13619 if (v.attrs[a] !== val) return false; 13620 for (const a2 of attrOrder) { 13621 if (a2 === a) continue; 13622 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13623 } 13624 return true; 13625 }); 13626 } 13627 const outOfStock = candidateVariant ? (window.isPreorderProduct && candidateVariant.preorder_remaining !== null ? candidateVariant.preorder_remaining <= 0 : candidateVariant.stock <= 0) : false; 13628 ch.classList.toggle('out', !disabled && outOfStock); 13629 const badge = ch.querySelector('.chip-badge'); 13630 if (badge) badge.hidden = !(candidateVariant && outOfStock); 13631 }); 13632 group.querySelectorAll('.v-chip').forEach(ch=>{ 13633 const input = ch.querySelector('input[type=radio]'); 13634 ch.classList.toggle('selected', input && input.checked); 13635 }); 13636 }); 13637 } 13638 updateSummary(sel); 13639 refreshOptions(sel); 13640 document.querySelectorAll('.v-chip').forEach(chip=>{const input=chip.querySelector('input[type=radio]');if(input&&input.checked){chip.classList.add('selected');}}); 13641 13642 const container = document.getElementById('pd-variants'); 13643 container && container.addEventListener('change', function(ev){ 13644 const radio = ev.target.closest('input[type=radio]'); 13645 if (!radio) return; 13646 sel = readSelection(); 13647 if (autoFillMissing(sel)) sel = readSelection(); 13648 const chosen = findVariant(sel); 13649 applyVariant(chosen || null, true); 13650 13651 updateSummary(sel); 13652 refreshOptions(sel); 13653 });
13654 13655window.applyVariant=applyVariant; 13656})(); 13657 13658(function cleanBrokenDescImages(){ 13659 const containers = [ 13660 document.querySelector('.pd-full-desc-body'), 13661 document.querySelector('.pd-short-desc') 13662 ].filter(Boolean); 13663 13664 containers.forEach(root => { 13665 const imgs = root.querySelectorAll('img'); 13666 imgs.forEach(img => { 13667 if (img.complete && img.naturalWidth === 0) { 13668 img.remove(); 13669 return; 13670 } 13671 img.addEventListener('error', () => { 13672 img.remove(); 13673 }, { once:true }); 13674 }); 13675 }); 13676})(); 13677 13678/* ====== Scroll To Top ====== */ 13679(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'})})})(); 13680 13681/* Format stok 10+ */ 13682function _0xk67l(n){n=parseInt(n||0,10)||0;return n>10?'10+':String(n)} 13683 13684/* Toggle popover stok per cabang */ 13685(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=''}} 13686function 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()} 13687a.addEventListener('click',function(h){h.preventDefault();b.classList.contains('show')?f():e()});if(c)c.addEventListener('click',f)})(); 13688 13689/* ===== Share Product Modal ===== */ 13690(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://norita.mx/",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
13690ncodeURIComponent(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)}})(); 13691 13692/* ===== Embedding variants (server â client) ===== */ 13693(function(){ 13694 window.__PD_VARIANTS_WITH_BRANCH__ = []; 13695})(); 13696 13697/* ===== Helper: update angka popover ===== */ 13698function updateStockPopoverForVariant(v){ 13699 try{ 13700 const elMeg = document.getElementById('sp-qty-meg'); 13701 const elMdf = document.getElementById('sp-qty-mdf'); 13702 const elBda = document.getElementById('sp-qty-bda'); 13703 13704 if (typeof v?.smm === 'number' && elMeg) elMeg.textContent = formatStockLabel(Math.max(0, v.smm|0)); 13705 if (typeof v?.smf === 'number' && elMdf) elMdf.textContent = formatStockLabel(Math.max(0, v.smf|0)); 13706 if (typeof v?.sba === 'number' && elBda) elBda.textContent = formatStockLabel(Math.max(0, v.sba|0)); 13707 }catch(_){} 13708} 13709 13710/* ===== applyVariant: 10+ dan update popover ===== */ 13711(function patchApplyVariant(){ 13712 if (typeof applyVariant !== 'function') return; 13713 const original = applyVariant; 13714 window.applyVariant = function(v){ 13715 original(v); 13716 13717 try { 13718 const elStock = document.getElementById('pd-stock-badge'); 13719 if (elStock){ 13720 let txt = elStock.textContent || ''; 13721 let m = txt.match(/\d+/); 13722 if (m) { 13723 const n = parseInt(m[0]||'0',10)||0; 13724 elStock.textContent = (n>0) ? ('Stok: ' + formatStockLabel(n)) : 'Habis'; 13725 } 13726 } 13727 } catch(_){} 13728if(window.__PD_VARIANTS_WITH_BRANCH__&&v){const vv=window.__PD_VARIANTS_WITH_BRANCH__.find(x=>x.id===v.id)||v;updateStockPopoverForVariant(vv)} 13729 }; 13730})(); 13731 13732(function initCurrentVariantBranch(){ 13733 const list = window.__PD_VARIANTS_WITH_BRANCH__ || []; 13734 if (!list.length) return; 13735 const hid = document.getElementById('pd-variant-id'); 13736 const vid = hid ? parseInt(hid.value||'0',10) : 0; 13737 const current = list.find(x => x.id === vid) || list[0] || null; 13738 if (current) updateStockPopoverForVariant(current); 13739})(); 13740 13741(function(){ 13742 if (!window.ga4Ecomm) return; 13743 const catSegs = ["Komputer & Laptop","Laptop","Gaming"]; 13744 window.ga4Ecomm.viewItem({ 13745 currency: 'IDR', 13746 value: 20789000, 13747 item_id: "SB0B3T", 13748 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", 13749 item_brand: "Lenovo ",
13750 item_category: catSegs && catSegs.length ? catSegs.join(' / ') : '' 13751 }); 13752 })(); 13753 13754(function patchApplyVariantFocus(){ 13755 if (typeof applyVariant !== 'function') return; 13756 const original = applyVariant; 13757 window.applyVariant = function(v, focusImage = true){ 13758 13759 original(v, focusImage); 13760 13761 if (focusImage && v && v.image_url) { 13762 _0xau1v(v.image_url); 13763 } 13764 }; 13765})(); 13766 13767(function() { 13768 var soldMap = []; 13769 var soldTotalLabel = "Terjual 2"; 13770 13771 function formatSoldLabel(qty) { 13772 if (qty >= 1000000) { 13773 var v = Math.floor((qty / 100000) ) / 10; 13774 return 'Terjual ' + (String(v).replace('.', ',')) + 'jt+'; 13775 } 13776 if (qty >= 1000) { 13777 var v2 = Math.floor((qty / 100) ) / 10; 13778 return 'Terjual ' + (String(v2).replace('.', ',')) + 'rb+'; 13779 } 13780 if (qty >= 100) { 13781 var v2 = Math.floor((qty / 100) ) * 100; 13782 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13783 } 13784 if (qty >= 10) { 13785 var v2 = Math.floor((qty / 10) ) * 10; 13786 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13787 } 13788 return 'Terjual ' + (qty.toLocaleString('id-ID')); 13789 } 13790 13791 function updateSoldLabel(labelText) { 13792 var el = document.querySelector('#pd-sold-inline, [data-role="sold-chip"]'); 13793 if (el) el.textContent = labelText; 13794 } 13795 13796 window.updateSoldBySku = function(sku) { 13797 if (!sku || typeof sku !== 'string') { 13798 updateSoldLabel(soldTotalLabel); 13799 return; 13800 } 13801 var qty = soldMap[sku] || 0; 13802 updateSoldLabel(formatSoldLabel(qty)); 13803 }; 13804 13805 updateSoldLabel(soldTotalLabel); 13806 })(); 13807 13808(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)}})} 13809function 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} 13810const 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}});
13810function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}})(); 13811 13812(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')}})(); 13813</script>
vendor: 2 bytes, line 13813
13813
13814<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13815window.PD_ID=325;window.PD_NAME=_0x3c4d;window.PD_SKU=_0x5e6f;window.PD_CATEGORY_PATH="Komputer & Laptop/Laptop/Gaming";window.PD_BEST_VOUCHER=null; 13816window.IS_DIGITAL=IS_DIGITAL; 13817window.isPreorderProduct=false; 13818window.PD_PREORDER_DATA=null; 13819</script>
13819 13820
13821<script data-inlined="pd-lightbox.js"> 13822// PlazaIT Product Lightbox (vanilla, no CDN) â v4 (swipe-down to close) 13823(function(){ 13824 const state = { 13825 items: [], 13826 open: false, 13827 idx: 0, 13828 scale: 1, 13829 minScale: 1, 13830 maxScale: 4, 13831 tx: 0, ty: 0, 13832 isDragging: false, 13833 dragStartX: 0, dragStartY: 0, 13834 baseTx: 0, baseTy: 0, 13835 touchStartX: 0, touchStartY: 0, 13836 lastTap: 0, 13837 swipeMode: 'none', // 'none' | 'h' | 'v' 13838 totalDx: 0, totalDy: 0 13839 }; 13840 13841 let root, imgEl, counterEl, btnPrev, btnNext, btnClose, canvas; 13842 13843 const ORIGIN = (location.origin || (location.protocol + '//' + location.host)); 13844 13845 function absUrl(u){ 13846 if (!u) return ''; 13847 try { 13848 if (/^https?:\/\//i.test(u)) return u; 13849 return new URL(u, ORIGIN).href; 13850 } catch(_) { return u; } 13851 } 13852 function fileName(u){ 13853 try { return new URL(u, ORIGIN).pathname.split('/').pop().toLowerCase(); } 13854 catch(_){ return String(u||'').split('/').pop().toLowerCase(); } 13855 } 13856 13857 function buildItems(){ 13858 const gd = (window.galleryData || []).filter(Boolean); 13859 if (gd.length){ 13860 state.items = gd.map(g => ({ 13861 src: absUrl(g.url), alt: g.alt || '', slug: g.slug || '', w: g.w || 1200, h: g.h || 1200 13862 })).filter(it => !!it.src); 13863 return; 13864 } 13865 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 13866 state.items = Array.from(thumbs).map(btn => { 13867 const raw = btn.getAttribute('data-full') || ''; 13868 return { src: absUrl(raw), alt: btn.getAttribute('data-alt') || '', slug: btn.getAttribute('data-slug') || '', w: 1200, h: 1200 }; 13869 }).filter(it => !!it.src); 13870 } 13871 13872 function ensureUI(){ 13873 if (root) return; 13874 root = document.createElement('div'); 13875 root.className = 'pd-lb'; 13876 root.id = 'pd-lightbox'; 13877 root.setAttribute('role','dialog'); 13878 root.setAttribute('aria-modal','true'); 13879 root.setAttribute('aria-hidden','true'); 13880 root.setAttribute('tabindex','-1'); 13881 13882 // Canvas terlebih dulu (z-index di CSS membuat tombol di atas) 13883 root.innerHTML = ` 13884 <div class="pd-lb-canvas"><img class="pd-lb-img" id="pd-lb-img" alt=""></div> 13885 <button class="pd-lb-close" aria-label="Tutup" title="Tutup">×</button> 13886 <button class="pd-lb-prev" aria-label="Sebelumnya" title="Sebelumnya">❮</button> 13887 <button class="pd-lb-next" aria-label="Berikutnya" title="Berikutnya">❯</button> 13888 <div class="pd-lb-counter" id="pd-lb-counter">1 / 1</div> 13889 `; 13890 document.body.appendChild(root); 13891 13892 imgEl = root.querySelector('#pd-lb-img'); 13893 counterEl = root.querySelector('#pd-lb-counter'); 13894 btnPrev = root.querySelector('.pd-lb-prev'); 13895 btnNext = root.querySelector('.pd-lb-next'); 13896 btnClose = root.querySelector('.pd-lb-close'); 13897 canvas = root.querySelector('.pd-lb-canvas'); 13898 13899 // Button clicks 13900 btnPrev.addEventListener('click', (e)=>{ e.stopPropagation(); prev(); }); 13901 btnNext.addEventListener('click', (e)=>{ e.stopPropagation(); next(); }); 13902 btnClose.addEventListener('click', (e)=>{ e.stopPropagation(); close(); }); 13903 13904 // Klik backdrop untuk tutup 13905 root.addEventListener('click', (e)=>{ if (e.target === root) close(); }); 13906 13907 // Keyboard global 13908 document.addEventListener('keydown', (e)=>{ 13909 if (!state.open) return; 13910 if (e.key === 'Escape') { e.preventDefault(); close(); } 13911 if (e.key === 'ArrowRight') { e.preventDefault(); next(); } 13912 if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } 13913 }); 13914 13915 // Wheel zoom (desktop) 13916 root.addEventListener('wheel', (e)=>{ 13917 if (!state.open) return; 13918 e.preventDefault(); 13919 const delta = -Math.sign(e.deltaY) * 0.2; 13920 zoomAt(delta, e.clientX, e.clientY); 13921 }, { passive: false }); 13922 13923 // Drag pan (mouse) 13924 canvas.addEventListener('mousedown', (e)=>{ 13925 if (state.scale <= 1) return; 13926 state.isDragging = true; 13927 state.dragStartX = e.clientX; 13928 state.dragStartY = e.clientY; 13929 state.baseTx = state.tx; 13930 state.baseTy = state.ty; 13931 e.preventDefault(); 13932 });
13933 window.addEventListener('mousemove', (e)=>{ 13934 if (!state.isDragging) return; 13935 const dx = e.clientX - state.dragStartX; 13936 const dy = e.clientY - state.dragStartY; 13937 setTranslate(state.baseTx + dx, state.baseTy + dy); 13938 }); 13939 window.addEventListener('mouseup', ()=>{ state.isDragging = false; }); 13940 13941 // Touch: swipe navigate (H), swipe-down close (V), double-tap zoom, drag pan 13942 canvas.addEventListener('touchstart', (e)=>{ 13943 if (!state.open) return; 13944 if (e.touches.length === 1) { 13945 const t = e.touches[0]; 13946 state.touchStartX = t.clientX; 13947 state.touchStartY = t.clientY; 13948 state.totalDx = 0; 13949 state.totalDy = 0; 13950 state.swipeMode = 'none'; 13951 13952 state.isDragging = state.scale > 1; 13953 state.dragStartX = t.clientX; 13954 state.dragStartY = t.clientY; 13955 state.baseTx = state.tx; 13956 state.baseTy = state.ty; 13957 13958 // Double-tap toggle zoom 13959 const now = Date.now(); 13960 if (now - state.lastTap < 300) { 13961 if (state.scale > 1) resetTransform(); else zoomAt(+1, t.clientX, t.clientY, 2); 13962 state.lastTap = 0; 13963 e.preventDefault(); 13964 return; 13965 } 13966 state.lastTap = now; 13967 } 13968 }, { passive: true }); 13969 13970 canvas.addEventListener('touchmove', (e)=>{ 13971 if (!state.open) return; 13972 13973 // Drag pan saat zoom 13974 if (state.isDragging && e.touches.length === 1 && state.scale > 1) { 13975 e.preventDefault(); 13976 const t = e.touches[0]; 13977 const dx = t.clientX - state.dragStartX; 13978 const dy = t.clientY - state.dragStartY; 13979 setTranslate(state.baseTx + dx, state.baseTy + dy); 13980 return; 13981 } 13982 13983 // Gesture saat scale <= 1 13984 if (state.scale <= 1 && e.touches.length === 1) { 13985 const t = e.touches[0]; 13986 const dx = t.clientX - state.touchStartX; 13987 const dy = t.clientY - state.touchStartY; 13988 state.totalDx = dx; state.totalDy = dy; 13989 13990 // Tentukan mode swipe di awal gerakan 13991 if (state.swipeMode === 'none') { 13992 const absX = Math.abs(dx), absY = Math.abs(dy); 13993 if (absY > 12 && absY > absX * 1.3) { 13994 state.swipeMode = 'v'; // swipe vertical (untuk close) 13995 } else if (absX > 12 && absX > absY * 1.3) { 13996 state.swipeMode = 'h'; // swipe horizontal (ganti slide) 13997 } 13998 } 13999 14000 if (state.swipeMode === 'v') { 14001 // Ikuti jari, kurangi opacity untuk feedback 14002 e.preventDefault(); 14003 setRootDrag(dy); 14004 } else if (state.swipeMode === 'h') { 14005 // Biarkan sampai touchend untuk menentukan next/prev (tidak perlu preventDefault di sini) 14006 } 14007 } 14008 }, { passive: false }); 14009 14010 canvas.addEventListener('touchend', (e)=>{ 14011 if (!state.open) return; 14012 14013 if (state.scale <= 1) { 14014 const dx = state.totalDx; 14015 const dy = state.totalDy; 14016 14017 if (state.swipeMode === 'v') { 14018 const TH = 80; // threshold jarak piksel untuk menutup 14019 if (Math.abs(dy) > TH) { 14020 // Animasi keluar arah swipe 14021 animateClose(dy); 14022 } else { 14023 // Snap back 14024 resetRootDrag(true); 14025 } 14026 } else if (state.swipeMode === 'h') { 14027 if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); } 14028 } 14029 } 14030 14031 state.isDragging = false; 14032 state.swipeMode = 'none'; 14033 state.totalDx = 0; 14034 state.totalDy = 0; 14035 }); 14036 } 14037 14038 function setImage(idx){ 14039 if (!state.items.length) return; 14040 state.idx = (idx + state.items.length) % state.items.length; 14041 const it = state.items[state.idx]; 14042 imgEl.src = it.src; 14043 imgEl.alt = it.alt || ''; 14044 counterEl.textContent = (state.idx + 1) + ' / ' + state.items.length; 14045 resetTransform(); 14046 preloadNeighbors(); 14047 } 14048 14049 function resetTransform(){ 14050 state.scale = 1; 14051 state.tx = 0; state.ty = 0; 14052 applyTransform(); 14053 } 14054 14055 function setTranslate(tx, ty){ 14056 state.tx = tx; state.ty = ty; 14057 applyTransform(); 14058 } 14059 14060 function applyTransform(){ 14061 imgEl.style.transform = `translate3d(${state.tx}px, ${state.ty}px, 0) scale(${state.scale})`; 14062 } 14063 14064 function zoomAt(delta, clientX, clientY, snapTo){ 14065 const old = state.scale; 14066 let next = (typeof snapTo === 'number') ? snapTo : (old + delta); 14067 next = Math.max(state.minScale, Math.min(state.maxScale, next)); 14068 14069 if (next !== old) { 14070 const rect = imgEl.getBoundingClientRect(); 14071 const cx = clientX - rect.left - rect.width/2; 14072 const cy = clientY - rect.top - rect.height/2; 14073 state.tx -= cx * (next/old - 1); 14074 state.ty -= cy * (next/old - 1); 14075 state.scale = next; 14076 applyTransform(); 14077 } 14078 } 14079 14080 function preloadNeighbors(){ 14081 if (state.items.length < 2) return; 14082 const nextIdx = (state.idx + 1) % state.items.length; 14083 const prevIdx = (state.idx - 1 + state.items.length) % state.items.length;
14084 [nextIdx, prevIdx].forEach(i=>{ 14085 const img = new Image(); img.src = state.items[i].src; 14086 }); 14087 } 14088 14089 // ----- Swipe-down visual helpers ----- 14090 function setRootDrag(dy){ 14091 const damped = dy * 0.6; // redam gerakan agar lembut 14092 const abs = Math.abs(dy); 14093 const fade = Math.max(0, Math.min(1, 1 - abs / 300)); // transparansi sesuai jarak 14094 root.style.transform = `translate3d(0, ${damped}px, 0)`; 14095 root.style.opacity = String(0.95 * fade + 0.05); // jangan jadi 0 total sebelum close 14096 } 14097 function resetRootDrag(withAnim){ 14098 if (withAnim) { 14099 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14100 root.style.transform = 'translate3d(0,0,0)'; 14101 root.style.opacity = '1'; 14102 setTimeout(()=>{ root.style.transition = ''; }, 190); 14103 } else { 14104 root.style.transition = ''; 14105 root.style.transform = ''; 14106 root.style.opacity = ''; 14107 } 14108 } 14109 function animateClose(dy){ 14110 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14111 const off = (dy > 0 ? window.innerHeight : -window.innerHeight); 14112 root.style.transform = `translate3d(0, ${off}px, 0)`; 14113 root.style.opacity = '0'; 14114 setTimeout(()=>{ root.style.transition = ''; close(); }, 180); 14115 } 14116 14117 function setSiblingsInert(makeInert){ 14118 // Inert semua saudara root (elemen lain di body) agar fokus tidak lari 14119 const nodes = Array.from(document.body.children).filter(el => el !== root); 14120 nodes.forEach(el=>{ 14121 if (makeInert) { 14122 if (!el.hasAttribute('inert')) el.setAttribute('inert',''); 14123 } else { 14124 if (el.hasAttribute('inert')) el.removeAttribute('inert'); 14125 } 14126 }); 14127 } 14128 14129 function open(idx){ 14130 if (!state.items.length) buildItems();
14131 if (!state.items.length) return; 14132 ensureUI(); 14133 14134 // Reset efek drag visual 14135 resetRootDrag(false); 14136 14137 document.body.style.overflow = 'hidden'; 14138 root.classList.add('show'); 14139 root.removeAttribute('aria-hidden'); // hindari aria-hidden saat fokus child 14140 setSiblingsInert(true); 14141 14142 state.open = true; 14143 setImage(typeof idx === 'number' ? idx : 0); 14144 14145 // Fokuskan dialog agar keyboard pasti tertangkap 14146 try { root.focus(); } catch(_){} 14147 } 14148 14149 function close(){ 14150 state.open = false; 14151 root.classList.remove('show'); 14152 root.setAttribute('aria-hidden','true'); 14153 setSiblingsInert(false); 14154 document.body.style.overflow = ''; 14155 14156 // Bersihkan transform/opacity 14157 resetRootDrag(false); 14158 } 14159 14160 function next(){ setImage(state.idx + 1); } 14161 function prev(){ setImage(state.idx - 1); } 14162 14163 function findIndexByUrl(url){ 14164 const target = fileName(absUrl(url)); 14165 const idx = state.items.findIndex(it => fileName(it.src) === target); 14166 return idx >= 0 ? idx : 0; 14167 } 14168 14169 function initPdLightbox(){ 14170 buildItems();
14171 ensureUI(); 14172 14173 // Klik gambar utama â buka LB di index yang sama 14174 const mainImg = document.getElementById('pd-main-img'); 14175 if (mainImg) { 14176 mainImg.style.cursor = 'zoom-in'; 14177 mainImg.addEventListener('click', ()=>{ 14178 open(findIndexByUrl(mainImg.src)); 14179 }); 14180 } 14181 14182 // Klik gambar slider mobile â buka LB di gambar yang diketuk 14183 const slider = document.getElementById('pd-mobile-slider'); 14184 if (slider) { 14185 slider.addEventListener('click', (e)=>{ 14186 const im = e.target && e.target.tagName === 'IMG' ? e.target : e.target.closest('img'); 14187 if (!im) return; 14188 open(findIndexByUrl(im.src)); 14189 }); 14190 } 14191 14192 // (Opsional) double-click pada thumbnail untuk buka langsung 14193 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 14194 thumbs.forEach((btn)=>{ 14195 let last = 0; 14196 btn.addEventListener('click', ()=>{ 14197 const now = Date.now(); 14198 if (now - last < 300) { 14199 const src = btn.getAttribute('data-full') || ''; 14200 open(findIndexByUrl(absUrl(src))); 14201 last = 0; 14202 } else { 14203 last = now; 14204 } 14205 }); 14206 }); 14207 14208 // Ekspos API 14209 window.PDLightbox = { open, close, next, prev, findIndexByUrl }; 14210 } 14211 14212 if (document.readyState === 'loading') { 14213 document.addEventListener('DOMContentLoaded', initPdLightbox); 14214 } else { 14215 initPdLightbox(); 14216 } 14217})(); 14218</script>
vendor: 2 bytes, line 14218
14218
14219<script data-inlined="cart.js"> 14220(function(){ 14221 function $(s,ctx=document){ return ctx.querySelector(s); } 14222 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 14223 14224 // CSRF token helper 14225 function getCsrfToken() { 14226 const metaTag = document.querySelector('meta[name="csrf-token"]'); 14227 return metaTag ? metaTag.getAttribute('content') : ''; 14228 } 14229 14230 function guessPdMeta(){ 14231 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 14232 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 14233 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 14234 const imgEl = $('#pd-main-img'); 14235 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 14236 return { name, sku, image_url }; // price removed for security 14237 } 14238 14239 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 14240 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 14241 const payload = { 14242 product_id: parseInt(productId||0,10) || 0, 14243 qty: parseInt(qty||1,10) || 1, 14244 variant_id: parseInt(variantId||0,10) || 0, 14245 meta 14246 }; 14247 14248 // Check if user is logged in 14249 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 14250 document.body.classList.contains('logged-in') || 14251 window.userLoggedIn === true || 14252 document.body.dataset.userLoggedIn === 'true'; 14253 14254 // Skip localStorage untuk logged user - mereka pakai database 14255 if (!isLoggedIn && window.guestCart) { 14256 try { 14257 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 14258 } catch (e) { 14259 console.warn('Failed to add to localStorage cart:', e); 14260 } 14261 } 14262 14263 const res = await fetch('/api/cart/add.php', { 14264 method: 'POST', 14265 headers: { 14266 'Content-Type':'application/json', 14267 'X-CSRF-Token': getCsrfToken() 14268 }, 14269 credentials: 'same-origin', 14270 body: JSON.stringify({ 14271 ...payload, 14272 csrf_token: getCsrfToken() 14273 }), 14274 }); 14275 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 14276 if (!res.ok || !data.ok) { 14277 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 14278 throw new Error(msg); 14279 } 14280 14281 // Update badge di navbar dengan data dari response 14282 try { 14283 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14284 if (el && data.cart && typeof data.cart.items_count === 'number') { 14285 el.textContent = String(data.cart.items_count); 14286 el.classList.toggle('is-empty', data.cart.items_count <= 0); 14287 } 14288 } catch(_){} 14289 14290 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 14291 try { 14292 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 14293 } catch(_){} 14294 14295 // Update CartBadge dengan data dari response (lebih reliable) 14296 try { 14297 if (window.CartBadge && window.CartBadge.set) { 14298 window.CartBadge.set(data.cart.items_count||0); 14299 } 14300 } catch(_){} 14301 14302 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 14303 setTimeout(() => { 14304 if (window.updateCartCounter) { 14305 window.updateCartCounter(); 14306 } 14307 }, 100); 14308 14309 return data; 14310 } 14311 14312 // Function to update cart counter from server 14313 async function updateCartCounter(){ 14314 try { 14315 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 14316 const data = await res.json().catch(()=>({ok:false})); 14317 if (data.ok && typeof data.items_count === 'number') { 14318 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14319 if (el) { 14320 el.textContent = String(data.items_count); 14321 el.classList.toggle('is-empty', data.items_count <= 0); 14322 } 14323 // Dispatch event 14324 try { 14325 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 14326 } catch(_){} 14327 } 14328 } catch (err) { 14329 console.log('Could not update cart counter:', err); 14330 } 14331 } 14332 14333 // Function to manually merge guest cart (if needed) 14334 async function mergeGuestCart(){ 14335 try { 14336 const res = await fetch('/api/cart/merge-guest.php', { 14337 method: 'POST', 14338 headers: { 14339 'Content-Type': 'application/json', 14340 'X-CSRF-Token': getCsrfToken() 14341 }, 14342 credentials: 'same-origin', 14343 body: JSON.stringify({ csrf_token: getCsrfToken() }) 14344 }); 14345 const data = await res.json().catch(()=>({ok:false})); 14346 14347 if (data.ok && data.merged_items > 0) { 14348 console.log(`Guest cart merged: ${data.merged_items} items`); 14349 // Update cart counter after merge 14350 await updateCartCounter(); 14351 return data; 14352 } 14353 return data; 14354 } catch (err) { 14355 console.log('Could not merge guest cart:', err); 14356 return { ok: false, error: err.message }; 14357 } 14358 } 14359 14360 window.addToCart = addToCart; 14361 window.updateCartCounter = updateCartCounter; 14362 window.mergeGuestCart = mergeGuestCart; 14363 14364 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 14365 document.addEventListener('click', async (e)=>{ 14366 const btn = e.target.closest('[data-addtocart]'); 14367 if (!btn) return; 14368 e.preventDefault(); 14369 if (btn.disabled) return; 14370 14371 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 14372 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
14373 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 14374 14375 try { 14376 const data = await addToCart(pid, qty, vid); 14377 if (window.showToast) { 14378 const meta = guessPdMeta(); 14379 window.showToast({ 14380 type:'success', 14381 title:'Berhasil ditambahkan', 14382 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 14383 duration: 3800, 14384 actions: [ 14385 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 14386 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 14387 ] 14388 }); 14389 } 14390 } catch (err) { 14391 if (window.showToast) { 14392 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 14393 } else { 14394 console.error(err); 14395 } 14396 } 14397 }); 14398})(); 14399</script>
vendor: 2 bytes, line 14399
14399
14400<script data-inlined="stock-validator-vanilla.js"> 14401/** 14402 * Stock Validation - Vanilla JS Version 14403 * 14404 * This script provides client-side stock validation without jQuery dependency 14405 * - Works with native JavaScript and fetch API 14406 * - Graceful fallback for older browsers 14407 * - Same functionality as jQuery version 14408 */ 14409 14410window.StockValidator = (function() { 14411 'use strict'; 14412 14413 const config = { 14414 apiEndpoint: '/api/stock/check.php', 14415 modalSelector: '#stock-modal-overlay', // We create our own modal 14416 loadingClass: 'loading-stock', 14417 disabledClass: 'stock-disabled', 14418 debug: true // Enable debug logging 14419 }; 14420 14421 /** 14422 * Cross-browser event listener helper 14423 */ 14424 function addEventListener(element, event, handler) { 14425 if (element.addEventListener) { 14426 element.addEventListener(event, handler); 14427 } else if (element.attachEvent) { 14428 element.attachEvent('on' + event, handler); 14429 } 14430 } 14431 14432 /** 14433 * Get stock information from embedded variant data (instant, no AJAX needed) 14434 */ 14435 function getStockFromEmbeddedData(variantId) { 14436 // Check if embedded variant data exists 14437 if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { 14438 return null; 14439 } 14440 14441 const variants = window.__PD_VARIANTS_WITH_BRANCH__; 14442 14443 // SPECIAL CASE: Simple product (no variants) - empty array means simple product 14444 if (variants.length === 0) { 14445 // For simple products, we cannot determine stock from embedded data 14446 // Return null to force API validation 14447 return null; 14448 } 14449 14450 // If no variant specified, check base product stock (all variants) 14451 if (!variantId || variantId <= 0) { 14452 // For base product, check if ANY variant has stock 14453 const isPreorder = window.isPreorderProduct || false; 14454 let hasAnyStock = false; 14455 let totalStock = 0; 14456 14457 if (isPreorder) { 14458 // For pre-order, check preorder_remaining 14459 hasAnyStock = variants.some(v => { 14460 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14461 return v.preorder_remaining > 0; 14462 } 14463 return v.stock > 0; 14464 }); 14465 totalStock = variants.reduce((sum, v) => { 14466 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14467 return sum + (v.preorder_remaining || 0); 14468 } 14469 return sum + (v.stock || 0); 14470 }, 0); 14471 } else { 14472 hasAnyStock = variants.some(v => v.stock > 0); 14473 totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); 14474 } 14475 14476 return { 14477 hasStock: hasAnyStock, 14478 stock: totalStock, 14479 sku: 'Base Product', 14480 variant: null 14481 }; 14482 } 14483 14484 // Find specific variant 14485 const variant = variants.find(v => v.id === variantId); 14486 14487 if (!variant) { 14488 return { 14489 hasStock: false, 14490 stock: 0, 14491 sku: 'Unknown Variant', 14492 variant: null 14493 }; 14494 } 14495 14496 // For pre-order products, check preorder_remaining instead of stock 14497 const isPreorder = window.isPreorderProduct || false; 14498 let hasStock = false; 14499 let availableStock = 0; 14500 14501 if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { 14502 availableStock = parseInt(variant.preorder_remaining, 10); 14503 if (isNaN(availableStock)) availableStock = 0; 14504 hasStock = availableStock > 0; 14505 } else { 14506 availableStock = variant.stock || 0; 14507 hasStock = availableStock > 0;
14508 } 14509 14510 return { 14511 hasStock: hasStock, 14512 stock: availableStock, 14513 sku: variant.sku, 14514 variant: variant 14515 }; 14516 } 14517 14518 /** 14519 * Cross-browser AJAX helper 14520 */ 14521 function makeRequest(url, options) { 14522 return new Promise((resolve, reject) => { 14523 // Try fetch first (modern browsers) 14524 if (typeof fetch !== 'undefined') { 14525 const fetchOptions = { 14526 method: options.method || 'POST', 14527 headers: { 14528 'Content-Type': 'application/x-www-form-urlencoded', 14529 }, 14530 body: options.data ? new URLSearchParams(options.data).toString() : null 14531 }; 14532 14533 fetch(url, fetchOptions) 14534 .then(response => { 14535 if (!response.ok) { 14536 throw new Error(`HTTP ${response.status}`); 14537 } 14538 return response.json(); 14539 }) 14540 .then(resolve) 14541 .catch(reject); 14542 } else { 14543 // Fallback to XMLHttpRequest 14544 const xhr = new XMLHttpRequest(); 14545 xhr.open(options.method || 'POST', url); 14546 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 14547 14548 xhr.onload = function() { 14549 if (xhr.status >= 200 && xhr.status < 300) { 14550 try { 14551 resolve(JSON.parse(xhr.responseText)); 14552 } catch (e) { 14553 reject(new Error('Invalid JSON response')); 14554 } 14555 } else { 14556 reject(new Error(`HTTP ${xhr.status}`)); 14557 } 14558 }; 14559 14560 xhr.onerror = function() { 14561 reject(new Error('Network error')); 14562 }; 14563 14564 const data = options.data ? new URLSearchParams(options.data).toString() : null; 14565 xhr.send(data); 14566 } 14567 }); 14568 } 14569 14570 /** 14571 * Check stock via AJAX 14572 */ 14573 function checkStock(productId, variantId, qty) { 14574 return makeRequest(config.apiEndpoint, { 14575 method: 'POST', 14576 data: { 14577 product_id: productId, 14578 variant_id: variantId || 0, 14579 qty: qty || 1 14580 } 14581 }); 14582 } 14583 14584 /** 14585 * Create and show stock alert modal using modal.css structure 14586 */ 14587 function showStockModal(stockInfo, action = 'add to cart') { 14588 let title, message, type, iconClass; 14589 14590 // Handle both string and object formats 14591 if (typeof stockInfo === 'string') { 14592 title = 'Stok Tidak Tersedia'; 14593 message = stockInfo; 14594 type = 'warning'; 14595 iconClass = 'warning'; 14596 } else if (stockInfo && typeof stockInfo === 'object') { 14597 if (!stockInfo.is_active) { 14598 title = 'Produk Tidak Tersedia'; 14599 message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; 14600 type = 'danger'; 14601 iconClass = 'danger'; 14602 } else if (!stockInfo.is_in_stock) { 14603 title = 'Stok Habis'; 14604 message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; 14605 if (stockInfo.sku) { 14606 message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`; 14607 } 14608 type = 'warning'; 14609 iconClass = 'warning'; 14610 } else if (!stockInfo.can_add_to_cart) { 14611 title = 'Stok Tidak Mencukupi'; 14612 message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`; 14613 message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`; 14614 message += `Silakan kurangi jumlah atau pilih varian lain.`; 14615 type = 'warning'; 14616 iconClass = 'warning'; 14617 } else { 14618 title = 'Stok Tidak Tersedia'; 14619 message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14620 type = 'warning'; 14621 iconClass = 'warning'; 14622 } 14623 } else { 14624 title = 'Stok Tidak Tersedia'; 14625 message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14626 type = 'warning'; 14627 iconClass = 'warning'; 14628 } 14629 14630 // Create modal HTML using modal.css structure 14631 const modalHTML = ` 14632 <div class="modal-overlay" id="stock-modal-overlay"> 14633 <div class="modal-dialog"> 14634 <div class="modal-header"> 14635 <div class="modal-icon ${iconClass}"> 14636 ${getIconSVG(iconClass)} 14637 </div> 14638 <h3 class="modal-title">${title}</h3> 14639 </div> 14640 <div class="modal-body"> 14641 <p class="modal-message">${message}</p> 14642 <div class="modal-actions"> 14643 <button type="button" class="modal-btn modal-btn-secondary" data-action="close"> 14644 Tutup 14645 </button> 14646 <button type="button" class="modal-btn modal-btn-primary" data-action="reload"> 14647 Muat Ulang 14648 </button> 14649 </div> 14650 </div> 14651 </div> 14652 </div> 14653 `; 14654 14655 // Remove existing modal if any 14656 const existingModal = document.getElementById('stock-modal-overlay'); 14657 if (existingModal) { 14658 existingModal.remove(); 14659 } 14660 14661 // Add modal to body 14662 document.body.insertAdjacentHTML('beforeend', modalHTML); 14663 14664 // Show modal with animation 14665 const modal = document.getElementById('stock-modal-overlay'); 14666 setTimeout(() => { 14667 modal.classList.add('show'); 14668 }, 10); 14669 14670 // Add event listeners to modal buttons 14671 const closeBtn = modal.querySelector('[data-action="close"]'); 14672 const reloadBtn = modal.querySelector('[data-action="reload"]'); 14673 14674 if (closeBtn) { 14675 addEventListener(closeBtn, 'click', closeStockModal); 14676 } 14677 14678 if (reloadBtn) { 14679 addEventListener(reloadBtn, 'click', function() { 14680 closeStockModal(); 14681 window.location.reload(); 14682 }); 14683 } 14684 14685 // Close modal when clicking overlay 14686 addEventListener(modal, 'click', function(event) { 14687 if (event.target === modal) { 14688 closeStockModal(); 14689 } 14690 }); 14691 14692 // Auto-close after 8 seconds 14693 setTimeout(() => { 14694 closeStockModal(); 14695 }, 8000); 14696 } 14697 14698 /** 14699 * Get icon SVG based on type 14700 */ 14701 function getIconSVG(type) { 14702 const icons = { 14703 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>', 14704 warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
14704<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>', 14705 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>', 14706 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>' 14707 }; 14708 return icons[type] || icons.info; 14709 } 14710 14711 /** 14712 * Close stock modal 14713 */ 14714 function closeStockModal() { 14715 const modal = document.getElementById('stock-modal-overlay'); 14716 if (modal) { 14717 modal.classList.remove('show'); 14718 setTimeout(() => { 14719 modal.remove(); 14720 }, 200); 14721 } 14722 } 14723 14724 // Make closeStockModal globally available 14725 if (typeof window !== 'undefined') { 14726 window.closeStockModal = closeStockModal; 14727 } 14728 14729 /** 14730 * Get element data attribute with fallback 14731 */ 14732 function getDataAttribute(element, attr) { 14733 if (element.dataset) { 14734 return element.dataset[attr]; 14735 } else { 14736 return element.getAttribute('data-' + attr); 14737 } 14738 } 14739 14740 /** 14741 * Validate stock before cart action 14742 */ 14743 async function validateBeforeCart(productId, variantId, qty, action = 'add') { 14744 try { 14745 // SMART VALIDATION: For simple products, check if we already have cached stock info 14746 // If buttons are enabled and cached info shows stock available, skip API validation 14747 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { 14748 14749 return true; 14750 } 14751 14752 // For variant products or when cached info is not available, do API validation 14753 const stockInfo = await checkStock(productId, variantId, qty); 14754 14755 if (!stockInfo.ok) { 14756 showStockModal({ 14757 message: stockInfo.error || 'Gagal mengecek stok', 14758 is_active: false 14759 }, action); 14760 return false; 14761 } 14762 14763 if (!stockInfo.can_add_to_cart) { 14764 showStockModal(stockInfo, action); 14765 return false; 14766 } 14767 14768 return true; // Stock available, proceed 14769 14770 } catch (error) { 14771 console.error('Stock validation error:', error); 14772 showStockModal({ 14773 message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', 14774 is_active: false 14775 }, action); 14776 return false; 14777 } 14778 } 14779 14780 /** 14781 * Handle button click with stock validation 14782 */ 14783 function handleButtonClick(event, action) { 14784 const button = event.target; 14785 14786 14787 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14788 if (button.hasAttribute('data-stock-blocked')) { 14789 event.preventDefault(); 14790 event.stopImmediatePropagation(); 14791 14792 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14793 showStockModal(reason); 14794 14795 // Show security warning 14796 if (window.showToast) { 14797 window.showToast({ 14798 type: 'warning', 14799 title: 'Aksi Tidak Diizinkan', 14800 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14801 duration: 3000 14802 }); 14803 } 14804 return false; 14805 } 14806 14807 // SECOND CHECK: Real-time stock validation against cached stock info 14808 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14809 event.preventDefault(); 14810 event.stopImmediatePropagation(); 14811 14812 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14813 14814 // Re-enforce button disabling 14815 button.disabled = true; 14816 button.style.opacity = '0.5'; 14817 button.style.cursor = 'not-allowed'; 14818 button.setAttribute('data-stock-blocked', 'true'); 14819 14820 return false; 14821 } 14822 14823 // Skip if already processing 14824 if (button.classList.contains(config.loadingClass)) { 14825 event.preventDefault(); 14826 return false; 14827 } 14828 14829 // Get product data from button attributes or form 14830 let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); 14831 let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); 14832 let qty = parseInt(getDataAttribute(button, 'qty') || 1); 14833 14834 // Try to get from global variables (set by product detail page) 14835 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14836 productId = parseInt(window.PD_ID || 0); 14837 } 14838 14839 // Try to get from parent form if not in button 14840 if (productId <= 0) { 14841 const form = button.closest ? button.closest('form') : null; 14842 if (form) { 14843 const pidAttr = form.getAttribute('data-pid'); 14844 const pidInput = form.querySelector('[name="product_id"]'); 14845 const vidInput = form.querySelector('[name="variant_id"]');
14846 const qtyInput = form.querySelector('[name="qty"]'); 14847 14848 if (pidAttr) productId = parseInt(pidAttr); 14849 if (pidInput) productId = parseInt(pidInput.value || 0); 14850 if (vidInput) variantId = parseInt(vidInput.value || 0); 14851 if (qtyInput) qty = parseInt(qtyInput.value || 1); 14852 } 14853 } 14854 14855 // CRITICAL: Always get current variant from selector (overrides button attributes) 14856 // This ensures we validate against the currently selected variant, not cached button data 14857 const variantSelector = document.getElementById('pd-variant-id'); 14858 if (variantSelector) { 14859 const currentVariantId = parseInt(variantSelector.value || 0); 14860 if (currentVariantId > 0) { 14861 variantId = currentVariantId; 14862 } 14863 } 14864 14865 14866 if (productId <= 0) { 14867 return true; // Let default handler deal with it 14868 } 14869 14870 // Prevent default and validate stock 14871 event.preventDefault(); 14872 event.stopPropagation(); 14873 14874 // Show loading state 14875 button.classList.add(config.loadingClass); 14876 const originalDisabled = button.disabled; 14877 button.disabled = true; 14878 14879 14880 validateBeforeCart(productId, variantId, qty, action) 14881 .then(isValid => { 14882 if (isValid) { 14883 // Stock OK, proceed with original action 14884 if (action === 'cart' && typeof window.addToCart === 'function') { 14885 window.addToCart(productId, qty, variantId); 14886 } else if (action === 'buy' && typeof window.buyNow === 'function') { 14887 // Call buyNow from pd-buynow.js without parameters (it gets data internally) 14888 window.buyNow(); 14889 } else { 14890 // Add fallback cart/buy logic here if needed 14891 } 14892 } 14893 }) 14894 .catch(error => { 14895 }) 14896 .finally(() => { 14897 // Remove loading state 14898 button.classList.remove(config.loadingClass); 14899 button.disabled = originalDisabled; 14900 }); 14901 14902 return false; 14903 } 14904 14905 /** 14906 * Handle mobile button click with stock validation 14907 */ 14908 function handleMobileButtonClick(event, action) { 14909 const button = event.target; 14910 14911 14912 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14913 if (button.hasAttribute('data-stock-blocked')) { 14914 event.preventDefault(); 14915 event.stopImmediatePropagation(); 14916 14917 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14918 showStockModal(reason); 14919 14920 // Show security warning 14921 if (window.showToast) { 14922 window.showToast({ 14923 type: 'warning', 14924 title: 'Aksi Tidak Diizinkan', 14925 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14926 duration: 3000 14927 }); 14928 } 14929 return false; 14930 } 14931 14932 // SECOND CHECK: Real-time stock validation against cached stock info 14933 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14934 event.preventDefault(); 14935 event.stopImmediatePropagation(); 14936 14937 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14938 14939 // Re-enforce button disabling 14940 button.disabled = true; 14941 button.style.opacity = '0.5'; 14942 button.style.cursor = 'not-allowed'; 14943 button.setAttribute('data-stock-blocked', 'true'); 14944 14945 return false; 14946 } 14947 14948 // Skip if already processing 14949 if (button.classList.contains(config.loadingClass)) { 14950 event.preventDefault(); 14951 return false; 14952 } 14953 14954 // Get product data from mobile FAB container 14955 let productId = 0; 14956 let variantId = 0; 14957 let qty = 1; 14958
14959 // Try to get from mobile FAB data-pid 14960 const mobileFab = document.getElementById('pd-mobile-fab'); 14961 if (mobileFab) { 14962 const pidAttr = getDataAttribute(mobileFab, 'pid'); 14963 if (pidAttr) productId = parseInt(pidAttr); 14964 } 14965 14966 // Try to get from global variables (set by product detail page) 14967 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14968 productId = parseInt(window.PD_ID || 0); 14969 } 14970 14971 // CRITICAL: Always get current variant from selector (real-time) 14972 // This ensures we validate against the currently selected variant 14973 const variantSelector = document.getElementById('pd-variant-id'); 14974 if (variantSelector) { 14975 variantId = parseInt(variantSelector.value || 0); 14976 } 14977 14978 // Try to get quantity from quantity input 14979 const qtySelector = document.querySelector('[name="qty"]'); 14980 if (qtySelector) { 14981 qty = parseInt(qtySelector.value || 1); 14982 } 14983 14984 // Ensure minimum quantity is 1 14985 qty = Math.max(1, qty); 14986 14987 14988 if (productId <= 0) { 14989 return true; // Let default handler deal with it 14990 } 14991 14992 // Event already prevented at interceptor level, just proceed with validation 14993 14994 // Show loading state 14995 button.classList.add(config.loadingClass); 14996 const originalDisabled = button.disabled; 14997 const originalText = button.textContent; 14998 button.disabled = true; 14999 button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; 15000 15001 15002 // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions 15003 if (window.addToCart && typeof window.addToCart === 'function') { 15004 window.addToCart._stockValidatorBlocked = true; 15005 } 15006 15007 // Also preemptively block other cart functions 15008 const originalAddToCartSubmit = window.addToCartSubmit; 15009 const originalPdAddToCartSubmit = window._pdAddToCartSubmit; 15010 15011 // Temporarily block ALL cart functions during validation 15012 window.addToCartSubmit = (...args) => { 15013 return Promise.resolve(false); 15014 }; 15015 15016 if (originalPdAddToCartSubmit) { 15017 window._pdAddToCartSubmit = (...args) => { 15018 return Promise.resolve(false); 15019 }; 15020 } 15021 15022 validateBeforeCart(productId, variantId, qty, action) 15023 .then(isValid => { 15024 if (isValid) { 15025 // Stock OK, proceed with original action 15026 15027 if (action === 'cart') { 15028 // For mobile cart, we need to recreate the original behavior 15029 // Strategy: Try multiple methods to ensure cart add + toast success 15030 15031 15032 let cartAddSuccess = false; 15033 let errorMessage = ''; 15034 15035 // Method 1: Try original addToCart function 15036 const originalAddToCart = window.addToCart && window.addToCart._original; 15037 if (originalAddToCart && !cartAddSuccess) { 15038 try { 15039 const result = originalAddToCart(productId, qty, variantId); 15040 15041 // Handle both sync and async returns 15042 if (result && typeof result.then === 'function') { 15043 result.then(() => { 15044 cartAddSuccess = true; 15045 }).catch(err => { 15046 errorMessage = err.message || 'addToCart failed'; 15047 }); 15048 } else { 15049 cartAddSuccess = true; 15050 } 15051 } catch (error) { 15052 errorMessage = error.message || 'addToCart exception'; 15053 } 15054 } 15055 15056 // Method 2: Try pd-addtocart.js function (backup method) 15057 if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { 15058 try { 15059 15060 // Create fake form for pd-addtocart.js 15061 const fakeForm = document.createElement('form');
15062 const qtyInput = document.createElement('input'); 15063 qtyInput.name = 'qty'; 15064 qtyInput.value = qty; 15065 fakeForm.appendChild(qtyInput); 15066 15067 // Set variant selector value for pd-addtocart to read 15068 const variantSelector = document.getElementById('pd-variant-id'); 15069 if (variantSelector) { 15070 variantSelector.value = variantId; 15071 } 15072 15073 const fakeEvent = { 15074 preventDefault: () => {}, 15075 target: fakeForm 15076 }; 15077 15078 // Call pd-addtocart.js function (this should handle toast internally) 15079 const result = window._pdAddToCartSubmit(fakeEvent, productId); 15080 if (result && typeof result.then === 'function') { 15081 result.then(() => { 15082 cartAddSuccess = true; 15083 }); 15084 } else { 15085 cartAddSuccess = true; 15086 } 15087 } catch (error) { 15088 } 15089 } 15090 15091 // Method 3: Direct addToCart call (fallback) 15092 if (typeof window.addToCart === 'function' && !cartAddSuccess) { 15093 try { 15094 const result = window.addToCart(productId, qty, variantId); 15095 if (result && typeof result.then === 'function') { 15096 result.then(() => { 15097 cartAddSuccess = true; 15098 }); 15099 } else { 15100 cartAddSuccess = true; 15101 } 15102 } catch (error) { 15103 } 15104 } 15105 15106 // Always show success toast for mobile (regardless of which method worked) 15107 // This ensures user gets feedback for successful cart addition 15108 setTimeout(() => { 15109 if (window.showToast) { 15110 15111 // Get product details for toast 15112 const getVariantSummaryText = () => { 15113 const vs = document.getElementById('pd-variant-summary'); 15114 if (!vs) return ''; 15115 const items = Array.from(vs.querySelectorAll('.vs-item strong')) 15116 .map(el => el.textContent.trim()) 15117 .filter(Boolean); 15118 return items.length ? items.join(' / ') : ''; 15119 }; 15120 15121 const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ 15122 '&':'&','<':'<','>':'>','"':'"',"'":''' 15123 }[m])); 15124 15125 const variantText = getVariantSummaryText(); 15126 const skuText = (function(){ 15127 const el = document.getElementById('pd-sku'); 15128 return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 15129 })(); 15130 const productName = window.PD_NAME || 'Product'; 15131 15132 window.showToast({ 15133 type: 'success', 15134 title: 'Berhasil ditambahkan ke Keranjang', 15135 message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` ÷ <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
15135 ÷ Qty: ${qty}`, 15136 duration: 5000, 15137 actions: [ 15138 { 15139 label: 'Lihat Keranjang', 15140 onClick: () => { window.location.href = '/keranjang'; } 15141 }, 15142 { 15143 label: 'Lanjut Belanja', 15144 variant: 'alt', 15145 onClick: () => {} 15146 } 15147 ] 15148 }); 15149 } else { 15150 } 15151 }, 100); // Small delay to ensure cart operation completes first 15152 15153 } else if (action === 'buy') { 15154 // For mobile buy, call buyNow 15155 if (typeof window.buyNow === 'function') { 15156 window.buyNow(); 15157 } 15158 } 15159 } else { 15160 // Stock validation FAILED - absolutely NO cart function calls allowed 15161 15162 // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) 15163 } 15164 15165 // Always restore functions after validation (success or failure) 15166 setTimeout(() => { 15167 // Remove blocking flag 15168 if (window.addToCart && typeof window.addToCart === 'function') { 15169 window.addToCart._stockValidatorBlocked = false; 15170 } 15171 15172 // Restore other functions 15173 if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { 15174 window.addToCartSubmit = originalAddToCartSubmit; 15175 } 15176 if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { 15177 window._pdAddToCartSubmit = originalPdAddToCartSubmit; 15178 } 15179 15180 }, 1000); // Shorter timeout since we want to restore quickly after success 15181 }) 15182 .catch(error => { 15183 // On error, also block the action 15184 }) 15185 .finally(() => { 15186 // Remove loading state 15187 button.classList.remove(config.loadingClass); 15188 button.disabled = originalDisabled; 15189 button.textContent = originalText; 15190 }); 15191 15192 return false; 15193 } 15194 15195 /** 15196 * Intercept cart buttons 15197 */ 15198 function interceptCartButtons() { 15199 15200 15201 // CRITICAL: Remove existing mobile cart handlers from product-detail.php 15202 setTimeout(() => { 15203 const mobileCartBtn = document.getElementById('mf-cart'); 15204 if (mobileCartBtn) { 15205 // Clone the button to remove ALL existing event listeners 15206 const newBtn = mobileCartBtn.cloneNode(true); 15207 mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); 15208 15209 15210 // Add our own handler to the new button 15211 newBtn.addEventListener('click', function(event) { 15212 event.preventDefault(); 15213 event.stopPropagation(); 15214 event.stopImmediatePropagation(); 15215 handleMobileButtonClick(event, 'cart'); 15216 return false; 15217 }, true); 15218 } 15219 }, 50); 15220 15221 // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) 15222 // This provides better integration with existing pd-addtocart.js toast system 15223 15224 // Buy now buttons - updated selectors (desktop + mobile) 15225 addEventListener(document, 'click', function(event) { 15226 const target = event.target; 15227 15228 // Desktop buy buttons 15229 if (target.classList.contains('btn-buy') || // Product detail buy button 15230 target.classList.contains('btn-buy-now') || 15231 target.classList.contains('buy-now-btn') || 15232 target.id === 'btn-buy' || // Specific ID match 15233 getDataAttribute(target, 'action') === 'buy-now' || 15234 getDataAttribute(target, 'action') === 'buy') { 15235 15236 handleButtonClick(event, 'buy'); 15237 } 15238 15239 // Mobile buy buttons 15240 else if (target.classList.contains('mf-buy') || // Mobile fab buy button 15241 target.id === 'mf-buy') { // Mobile buy button ID 15242 15243 handleMobileButtonClick(event, 'buy'); 15244 } 15245 15246 // Mobile cart buttons - ENHANCED BLOCKING 15247 else if (target.classList.contains('mf-cart') || // Mobile fab cart button 15248 target.id === 'mf-cart') { // Mobile cart button ID 15249 15250 15251 // Immediately prevent all event propagation 15252 event.preventDefault(); 15253 event.stopPropagation(); 15254 event.stopImmediatePropagation(); 15255 15256 handleMobileButtonClick(event, 'cart'); 15257 15258 // Return false to ensure no other handlers run 15259 return false; 15260 } 15261 }, true);
15261 // Use capturing phase to ensure we intercept before other handlers 15262 15263 // Additional protection: Override addToCart function to add mobile button state checking 15264 const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; 15265 if (originalAddToCart) { 15266 window.addToCart = async function(...args) { 15267 // Check if this call is coming from a mobile button that's currently being validated 15268 const mobileCartBtn = document.getElementById('mf-cart'); 15269 if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { 15270 return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); 15271 } 15272 15273 // Check if functions are temporarily blocked (during stock failure) 15274 if (window.addToCart._stockValidatorBlocked) { 15275 return Promise.reject(new Error('Stock validation failed - cart action blocked')); 15276 } 15277 15278 // Allow normal calls 15279 return originalAddToCart.apply(this, args); 15280 }; 15281 15282 // Store reference to original for restoration 15283 window.addToCart._original = originalAddToCart; 15284 } 15285 } 15286 15287 /** 15288 * Setup global click interceptor for security 15289 * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler 15290 */ 15291 function setupGlobalClickInterceptor() { 15292 // Add capture-phase listener to catch clicks before other handlers 15293 document.addEventListener('click', function(event) { 15294 const target = event.target; 15295 15296 // Check if this is a cart/buy button 15297 const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || 15298 target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); 15299 const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || 15300 target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); 15301 15302 if (!isCartButton && !isBuyButton) { 15303 return; // Not our target, allow normal processing 15304 } 15305 15306 const button = isCartButton ? 15307 (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')) : 15308 (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); 15309 15310 // IMMEDIATE BLOCK: Prevent any processing first 15311 event.preventDefault(); 15312 event.stopImmediatePropagation(); 15313 15314 // SECURITY CHECK 1: Is button marked as stock-blocked? 15315 if (button && button.hasAttribute('data-stock-blocked')) { 15316 15317 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 15318 showStockModal(reason); 15319 15320 // Show security alert 15321 if (window.showToast) { 15322 window.showToast({ 15323 type: 'error', 15324 title: 'Aksi Diblokir', 15325 message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', 15326 duration: 4000 15327 }); 15328 } 15329 15330 return false; 15331 } 15332 15333 // SECURITY CHECK 2: Real-time validation against cached stock info 15334 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 15335 15336 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 15337 15338 // Re-enforce button disabling 15339 if (button) { 15340 button.disabled = true; 15341 button.style.opacity = '0.5'; 15342 button.style.cursor = 'not-allowed'; 15343 button.setAttribute('data-stock-blocked', 'true'); 15344 button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); 15345 } 15346 15347 return false; 15348 } 15349 15350 // SECURITY CHECK 3: Smart client-side validation 15351 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15352 const variantSelector = document.getElementById('pd-variant-id'); 15353 const variantId = variantSelector ? parseInt(variantSelector.value || 0
15353) : 0; 15354 15355 if (productId > 0) { 15356 // SMART VALIDATION: If we already have cached stock info showing stock is available, 15357 // and buttons are enabled, proceed immediately without re-validation 15358 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 15359 button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15360 15361 // Allow the action to proceed immediately with toast 15362 if (isCartButton) { 15363 // Trigger cart action with proper toast handling 15364 if (typeof window._pdAddToCartSubmit === 'function') { 15365 // Use pd-addtocart.js function for proper toast handling 15366 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15367 if (form) { 15368 const fakeEvent = { 15369 target: form, 15370 preventDefault: () => {} 15371 }; 15372 window._pdAddToCartSubmit(fakeEvent, productId); 15373 } 15374 } else if (typeof window.addToCartSubmit === 'function') { 15375 // Fallback to addToCartSubmit 15376 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15377 if (form) { 15378 const fakeEvent = { 15379 target: form, 15380 preventDefault: () => {} 15381 }; 15382 window.addToCartSubmit(fakeEvent, productId); 15383 } 15384 } else if (typeof window.addToCart === 'function') { 15385 // Direct addToCart call 15386 window.addToCart(productId, 1, variantId); 15387 } 15388 } else if (isBuyButton) { 15389 // Trigger buy action 15390 if (typeof window.buyNow === 'function') { 15391 window.buyNow(); 15392 } 15393 } 15394 15395 return false; // Action completed 15396 } 15397 15398 // Get stock info from embedded variant data (for variant products) 15399 const stockInfo = getStockFromEmbeddedData(variantId); 15400 15401 if (stockInfo && stockInfo.hasStock) { 15402 15403 // Update global stock cache 15404 window._currentStockInfo = { 15405 can_add_to_cart: true, 15406 is_in_stock: true, 15407 available_stock: stockInfo.stock, 15408 message: 'Stok tersedia' 15409 }; 15410 15411 // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) 15412 // If data-stock-blocked is set, respect it and don't enable the button 15413 const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; 15414 15415 if (!isExplicitlyBlocked) { 15416 // Remove stock blocking if it was set and enable button 15417 button.removeAttribute('data-stock-blocked'); 15418 button.removeAttribute('data-stock-reason'); 15419 button.disabled = false; 15420 button.style.opacity = ''; 15421 button.style.cursor = ''; 15422 button.classList.remove(config.disabledClass); 15423 } else { 15424 // Button is blocked by variant logic, show reason 15425 const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; 15426 console.log('Button blocked by variant logic:', reason); 15427 e.preventDefault(); 15428 e.stopPropagation(); 15429 return false; 15430 } 15431 15432 // Allow the action to proceed immediately with toast 15433 if (isCartButton) { 15434 // Trigger cart action with proper toast handling 15435 if (typeof window._pdAddToCartSubmit === 'function') { 15436 // Use pd-addtocart.js function for proper toast handling 15437 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15438 if (form) { 15439 const fakeEvent = { 15440 target: form, 15441 preventDefault: () => {} 15442 }; 15443 window._pdAddToCartSubmit(fakeEvent, productId); 15444 } 15445 } else if (typeof window.addToCartSubmit === 'function') { 15446 // Fallback to addToCartSubmit 15447 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15448 if (form) { 15449 const fakeEvent = { 15450 target: form, 15451 preventDefault: () => {} 15452 }; 15453 window.addToCartSubmit(fakeEvent, productId); 15454 } 15455 } else if (typeof window.addToCart === 'function') { 15456 // Direct addToCart call 15457 window.addToCart(productId, 1, variantId); 15458 } 15459 } else if (isBuyButton) { 15460 // Trigger buy action 15461 if (typeof window.buyNow === 'function') { 15462 window.buyNow(); 15463 } 15464 } 15465 } else if (stockInfo && !stockInfo.hasStock) { 15466 15467 // Update global stock cache 15468 window._currentStockInfo = { 15469 can_add_to_cart: false, 15470 is_in_stock: false, 15471 available_stock: stockInfo.stock, 15472 message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` 15473 }; 15474 15475 // Force block button 15476 button.disabled = true; 15477 button.style.opacity = '0.5'; 15478 button.style.cursor = 'not-allowed'; 15479 button.setAttribute('data-stock-blocked', 'true'); 15480 button.setAttribute('data-stock-reason', window._currentStockInfo.message); 15481 15482 // Show modal with variant info 15483 const modalMessage = stockInfo ? 15484 `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 15485 'Produk ini sedang habis stok. Silakan pilih varian lain.'; 15486 15487 showStockModal(modalMessage); 15488 15489 // Show security warning 15490 if (window.showToast) { 15491 window.showToast({ 15492 type: 'warning',
15493 title: 'Stok Tidak Tersedia', 15494 message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', 15495 duration: 3000 15496 }); 15497 } 15498 } else { 15499 // stockInfo is null (simple product) - this means embedded data is not available 15500 // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) 15501 if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15502 15503 // Allow the action to proceed immediately with toast 15504 if (isCartButton) { 15505 // Trigger cart action with proper toast handling 15506 if (typeof window._pdAddToCartSubmit === 'function') { 15507 // Use pd-addtocart.js function for proper toast handling 15508 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15509 if (form) { 15510 const fakeEvent = { 15511 target: form, 15512 preventDefault: () => {} 15513 }; 15514 window._pdAddToCartSubmit(fakeEvent, productId); 15515 } 15516 } else if (typeof window.addToCartSubmit === 'function') { 15517 // Fallback to addToCartSubmit 15518 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15519 if (form) { 15520 const fakeEvent = { 15521 target: form, 15522 preventDefault: () => {} 15523 }; 15524 window.addToCartSubmit(fakeEvent, productId); 15525 } 15526 } else if (typeof window.addToCart === 'function') { 15527 // Direct addToCart call 15528 window.addToCart(productId, 1, variantId); 15529 } 15530 } else if (isBuyButton) { 15531 // Trigger buy action 15532 if (typeof window.buyNow === 'function') { 15533 window.buyNow(); 15534 } 15535 } 15536 } else { 15537 // Button is disabled, don't allow action 15538 showStockModal('Produk ini sedang tidak tersedia.'); 15539 } 15540 } 15541 } else { 15542 // No product ID, block action 15543 showStockModal('Data produk tidak valid.'); 15544 } 15545 15546 return false; // Always block at capture phase 15547 15548 }, true); // Use capture phase to run BEFORE other listeners 15549 15550 15551 } 15552 15553 /** 15554 * Initialize button data attributes from current page 15555 */ 15556 function initializeButtonAttributes() { 15557 // Get product data from page globals or form 15558 let productId = 0, variantId = 0; 15559 15560 // Try to get from global variables (set by PHP) 15561 if (window.PD_ID) productId = parseInt(window.PD_ID); 15562 15563 // Try to get from form 15564 const form = document.getElementById('pd-buy-form'); 15565 if (form) { 15566 const pidAttr = form.getAttribute('data-pid'); 15567 if (pidAttr) productId = parseInt(pidAttr); 15568 15569 const varInput = document.getElementById('pd-variant-id'); 15570 if (varInput) variantId = parseInt(varInput.value || 0); 15571 } 15572 15573 // Try to get from mobile FAB 15574 const mobileFab = document.getElementById('pd-mobile-fab'); 15575 if (mobileFab && productId <= 0) { 15576 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15577 if (pidAttr) productId = parseInt(pidAttr); 15578 } 15579 15580 // Set attributes on desktop buttons 15581 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15582 desktopButtons.forEach(button => { 15583 if (productId > 0) { 15584 button.setAttribute('data-product-id', productId); 15585 if (variantId > 0) { 15586 button.setAttribute('data-variant-id', variantId); 15587 } 15588 } 15589 }); 15590 15591 // Set attributes on mobile buttons 15592 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15593 mobileButtons.forEach(button => { 15594 if (productId > 0) { 15595 button.setAttribute('data-product-id', productId); 15596 if (variantId > 0) { 15597 button.setAttribute('data-variant-id', variantId); 15598 } 15599 } 15600 }); 15601 15602 15603 15604 // Initial stock display update on page load 15605 if (productId > 0) { 15606 setTimeout(() => { 15607 updateStockDisplay(productId, variantId); 15608 15609 }, 100); 15610 } 15611 } 15612 15613 /** 15614 * Update button attributes when variant changes 15615 */ 15616 function updateButtonAttributesForVariant(variantId) { 15617 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15618 15619 if (productId <= 0) return; 15620 15621 // Update desktop buttons 15622 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15623 desktopButtons.forEach(button => { 15624 button.setAttribute('data-product-id', productId); 15625 if (variantId > 0) { 15626 button.setAttribute('data-variant-id', variantId); 15627 } else { 15628 button.removeAttribute('data-variant-id'); 15629 } 15630 }); 15631 15632 // Update mobile buttons 15633 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15634 mobileButtons.forEach(button => { 15635 button.setAttribute('data-product-id', productId); 15636 if (variantId > 0) { 15637 button.setAttribute('data-variant-id', variantId); 15638 } else { 15639 button.removeAttribute('data-variant-id'); 15640 } 15641 }); 15642 15643 15644 } 15645 15646 /** 15647 * Setup variant change integration with existing product-detail.php handlers 15648 */ 15649 function setupVariantChangeIntegration() { 15650 // Strategy: Hook into existing applyVariant function instead of adding new listener 15651 // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) 15652 15653 // Method 1: Hook into existing applyVariant function 15654 setTimeout(() => { 15655 if (typeof window.applyVariant === 'function') { 15656 const originalApplyVariant = window.applyVariant; 15657 window.applyVariant = function(v, focusImage) { 15658 // Call original function first 15659 const result = originalApplyVariant.call(this, v, focusImage); 15660 15661 // Then add our stock validator logic 15662 const variantId = v ? (v.id || v.ID || 0) : 0; 15663 15664 // Update button attributes for stock validator 15665 updateButtonAttributesForVariant(variantId); 15666 15667 // Update stock display using embedded data (instant) 15668 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15669 if (productId > 0) { 15670 updateStockDisplay(productId, variantId); 15671 } return result; 15672 }; 15673 15674 15675 } else { 15676 15677 setupVariantObserver(); 15678 } 15679 }, 500); 15680 15681 // Method 2: Direct observation of hidden input value changes (fallback) 15682 setTimeout(() => { 15683 setupVariantObserver(); 15684 }, 1000); 15685 } 15686 15687 /** 15688 * Setup variant observer as fallback when applyVariant hook fail
15688s 15689 */ 15690 function setupVariantObserver() { 15691 const variantInput = document.getElementById('pd-variant-id'); 15692 if (variantInput) { 15693 let lastVariantId = parseInt(variantInput.value || '0'); 15694 15695 // Use MutationObserver to watch for value changes 15696 if (window.MutationObserver) { 15697 const observer = new MutationObserver((mutations) => { 15698 mutations.forEach((mutation) => { 15699 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 15700 const newVariantId = parseInt(variantInput.value || '0'); 15701 if (newVariantId !== lastVariantId) { 15702 15703 updateButtonAttributesForVariant(newVariantId); 15704 15705 // Update stock display with embedded data 15706 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15707 if (productId > 0) { 15708 updateStockDisplay(productId, newVariantId); 15709 } 15710 15711 lastVariantId = newVariantId; 15712 } 15713 } 15714 }); 15715 }); 15716 15717 observer.observe(variantInput, { 15718 attributes: true, 15719 attributeFilter: ['value'] 15720 }); 15721 15722 15723 } 15724 15725 // Additional polling as ultimate fallback 15726 setInterval(() => { 15727 const currentVariantId = parseInt(variantInput.value || '0'); 15728 if (currentVariantId !== lastVariantId) { 15729 15730 updateButtonAttributesForVariant(currentVariantId); 15731 15732 // Update stock display with embedded data 15733 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15734 if (productId > 0) { 15735 updateStockDisplay(productId, currentVariantId); 15736 } 15737 15738 lastVariantId = currentVariantId; 15739 } 15740 }, 1000); 15741 15742 15743 } 15744 } 15745 15746 /** 15747 * Update stock display and enforce stock-based button blocking using embedded data 15748 */ 15749 function updateStockDisplay(productId, variantId = 0) { 15750 15751 15752 // Get stock info from embedded data (instant) 15753 const embeddedStockInfo = getStockFromEmbeddedData(variantId); 15754 15755 let stockInfo; 15756 if (embeddedStockInfo) { 15757 // Convert embedded data to standard format 15758 stockInfo = { 15759 can_add_to_cart: embeddedStockInfo.hasStock, 15760 is_in_stock: embeddedStockInfo.hasStock, 15761 available_stock: embeddedStockInfo.stock, 15762 message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, 15763 product_name: window.PD_NAME || 'Produk', 15764 sku: embeddedStockInfo.sku 15765 }; 15766 15767 15768 } else { 15769 // Fallback to API call if embedded data not available 15770 15771 checkStock(productId, variantId, 1) 15772 .then(apiStockInfo => { 15773 updateStockDisplayWithInfo(apiStockInfo, productId, variantId); 15774 }) 15775 .catch(error => { 15776 console.error('Failed to update stock display via API:', error); 15777 }); 15778 return; 15779 } 15780 15781 updateStockDisplayWithInfo(stockInfo, productId, variantId); 15782 } 15783 15784 /** 15785 * Update stock display with provided stock info 15786 */ 15787 function updateStockDisplayWithInfo(stockInfo, productId, variantId) { 15788 // Store current stock status globally for blocking checks 15789 window._currentStockInfo = stockInfo; 15790 15791 // Update stock indicators 15792 const stockIndicators = document.querySelectorAll('.stock-indicator');
15793 stockIndicators.forEach(indicator => { 15794 if (stockInfo.is_in_stock) { 15795 indicator.classList.add('in-stock'); 15796 indicator.classList.remove('out-of-stock'); 15797 } else { 15798 indicator.classList.add('out-of-stock'); 15799 indicator.classList.remove('in-stock'); 15800 } 15801 }); 15802 15803 // Update stock count 15804 const stockCounts = document.querySelectorAll('.stock-count'); 15805 stockCounts.forEach(count => { 15806 count.textContent = stockInfo.available_stock || 0; 15807 }); 15808 15809 // CRITICAL: For digital products, DO NOT modify button states here 15810 // PHP already handles button disable/enable based on LICENSE_POOL count 15811 // Only physical products need JavaScript stock validation 15812 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15813 15814 if (isDigitalProduct) { 15815 // For digital products: ONLY update global stock info, don't touch buttons 15816 // Let PHP-rendered disabled state remain authoritative 15817 console.log('[Stock Validator] Digital product detected - skipping button state modification'); 15818 return; // Exit early, preserve PHP-set button states 15819 } 15820 15821 // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 15822 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'); 15823 allButtons.forEach(button => { 15824 if (stockInfo.can_add_to_cart) { 15825 // STOCK AVAILABLE - ENABLE BUTTONS 15826 button.classList.remove(config.disabledClass); 15827 button.disabled = false; 15828 button.style.opacity = ''; 15829 button.style.cursor = ''; 15830 15831 // CRITICAL: Remove stock blocking attributes when stock becomes available 15832 button.removeAttribute('data-stock-blocked'); 15833 button.removeAttribute('data-stock-reason'); 15834 15835 15836 } else { 15837 // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING 15838 button.classList.add(config.disabledClass); 15839 button.disabled = true; 15840 button.style.opacity = '0.5'; 15841 button.style.cursor = 'not-allowed'; 15842 15843 // Mark as stock-blocked to prevent re-enabling 15844 button.setAttribute('data-stock-blocked', 'true'); 15845 button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); 15846 15847 15848 } 15849 }); 15850 15851 // Start aggressive button monitoring for out-of-stock variants 15852 if (!stockInfo.can_add_to_cart) { 15853 enforceStockDisabling(productId, variantId); 15854 } 15855 15856 15857 } 15858 15859 /** 15860 * Aggressively enforce button disabling for out-of-stock items 15861 * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) 15862 */ 15863 function enforceStockDisabling(productId, variantId) { 15864 // CRITICAL: Skip enforcement for digital products - PHP handles button states 15865 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15866 if (isDigitalProduct) { 15867 console.log('[Stock Validator] Digital product - skipping button enforcement'); 15868 return; // Exit early, let PHP-rendered disabled state remain 15869 } 15870 15871 const monitorInterval = setInterval(() => { 15872 // Check if variant changed 15873 const currentVariantSelector = document.getElementById('pd-variant-id'); 15874 const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; 15875 15876 if (currentVariantId !== variantId) { 15877 // Variant changed, stop monitoring this variant 15878 clearInterval(monitorInterval); 15879 15880 return; 15881 } 15882 15883 // Check current stock status using embedded data (real-time check) 15884 const currentStockInfo = getStockFromEmbeddedData(currentVariantId); 15885 15886 if (currentStockInfo && !currentStockInfo.hasStock) { 15887 // Still out of stock - continue monitoring for tampering 15888 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'); 15889 buttons.forEach(button => { 15890 // Check if button was manually enabled (inspect attack) 15891 if (!button.disabled || button.style.opacity !== '0.5') { 15892 15893 // Force re-disable 15894 button.disabled = true; 15895 button.style.opacity = '0.5'; 15896 button.style.cursor = 'not-allowed'; 15897 button.setAttribute('data-stock-blocked', 'true'); 15898 button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); 15899 15900 // Show warning toast 15901 if (window.showToast) { 15902 window.showToast({ 15903 type: 'warning',
15904 title: 'Aksi Tidak Diizinkan', 15905 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 15906 duration: 3000 15907 }); 15908 } 15909 } 15910 }); 15911 } else { 15912 // Stock available OR embedded data shows stock - stop monitoring 15913 clearInterval(monitorInterval); 15914 15915 // For PHYSICAL products only, enable buttons 15916 // For DIGITAL products, skip (PHP is authoritative) 15917 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15918 15919 if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 15920 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'); 15921 buttons.forEach(button => { 15922 button.disabled = false; 15923 button.style.opacity = ''; 15924 button.style.cursor = ''; 15925 button.removeAttribute('data-stock-blocked'); 15926 button.removeAttribute('data-stock-reason'); 15927 button.classList.remove(config.disabledClass); 15928 }); 15929 15930 } 15931 } 15932 }, 500); // Check every 500ms for tampering attempts 15933 15934 // Auto-stop monitoring after 30 seconds to prevent memory leaks 15935 setTimeout(() => { 15936 clearInterval(monitorInterval); 15937 15938 }, 30000); 15939 } 15940 15941 /** 15942 * Override addToCartSubmit function to add stock validation 15943 * This function now works with pd-addtocart.js integration 15944 */ 15945 function overrideAddToCartSubmitFunction() { 15946 // Check if pd-addtocart.js is already loaded 15947 const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 15948 window.addToCartSubmit.toString().includes('showToast'); 15949 15950 if (pdAddToCartExists) { 15951 15952 15953 // Store the pd-addtocart.js function 15954 const pdAddToCartSubmit = window.addToCartSubmit; 15955 window._pdAddToCartSubmit = pdAddToCartSubmit; 15956 15957 // Override with stock validation that calls pd-addtocart.js 15958 window.addToCartSubmit = async function(e, productId) { 15959 15960 // Don't prevent default here - let pd-addtocart handle it if stock is valid 15961 15962 try { 15963 // Extract product data from form (same way as pd-addtocart.js) 15964 const form = e.target; 15965 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 15966 15967 // CRITICAL: Always get current variant from selector (real-time) 15968 let variantId = 0; 15969 const variantSelector = document.getElementById('pd-variant-id'); 15970 if (variantSelector) { 15971 variantId = parseInt(variantSelector.value || '0', 10) || 0; 15972 } 15973 15974 if (!productId || productId <= 0) { 15975 return pdAddToCartSubmit(e, productId); 15976 } 15977 15978 15979 // Validate stock before proceeding 15980 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 15981 15982 if (isValid) { 15983 // Stock OK, call pd-addtocart.js function (it will handle toast) 15984 return pdAddToCartSubmit(e, productId); 15985 } else { 15986 // Prevent the form submission 15987 e.preventDefault(); 15988 // Stock validation failed, modal already shown by validateBeforeCart 15989 return false; 15990 } 15991 15992 } catch (error) { 15993 e.preventDefault(); 15994 // On error, show modal and don't proceed 15995 showStockModal({
15996 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 15997 is_active: false 15998 }, 'cart'); 15999 return false; 16000 } 16001 }; 16002 } else { 16003 // Original logic for when pd-addtocart.js is not present 16004 const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; 16005 16006 if (originalAddToCartSubmit) { 16007 window._originalAddToCartSubmit = originalAddToCartSubmit; 16008 } 16009 16010 // Override with stock validation 16011 window.addToCartSubmit = async function(e, productId) { 16012 16013 e.preventDefault(); 16014 16015 try { 16016 const form = e.target; 16017 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16018 16019 // CRITICAL: Always get current variant from selector (real-time) 16020 let variantId = 0; 16021 const variantSelector = document.getElementById('pd-variant-id'); 16022 if (variantSelector) { 16023 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16024 } 16025 16026 if (!productId || productId <= 0) { 16027 if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); 16028 return false; 16029 } 16030 16031 16032 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16033 16034 if (isValid) { 16035 if (originalAddToCartSubmit) { 16036 return originalAddToCartSubmit(e, productId); 16037 } else { 16038 if (typeof window.addToCart === 'function') { 16039 await window.addToCart(productId, qty, variantId); 16040 } 16041 } 16042 } else { 16043 } 16044 16045 } catch (error) { 16046 showStockModal({ 16047 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16048 is_active: false 16049 }, 'cart'); 16050 } 16051 16052 return false; 16053 }; 16054 } 16055 16056 16057 } 16058 16059 /** 16060 * Override buyNow function to add stock validation 16061 */ 16062 function overrideBuyNowFunction() { 16063 // Store original buyNow function if it exists 16064 const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; 16065 16066 // Override with stock validation 16067 window.buyNow = async function(e) { 16068 16069 try { 16070 // Get product data (same way as pd-buynow.js) 16071 const form = document.getElementById('pd-buy-form'); 16072 if (!form) { 16073 if (originalBuyNow) return originalBuyNow(e); 16074 return; 16075 } 16076 16077 const productId = parseInt(form.dataset.pid || '0', 10); 16078 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16079 16080 // CRITICAL: Always get current variant from selector (real-time) 16081 let variantId = 0; 16082 const variantSelector = document.getElementById('pd-variant-id'); 16083 if (variantSelector) { 16084 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16085 } 16086 16087 if (!productId || productId <= 0) { 16088 if (originalBuyNow) return originalBuyNow(e); 16089 return; 16090 } 16091 16092 16093 // Validate stock before proceeding 16094 const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); 16095 16096 if (isValid) { 16097 // Stock OK, call original buyNow function 16098 if (originalBuyNow) { 16099 return originalBuyNow(e); 16100 } else { 16101 } 16102 } else { 16103 // Stock validation failed, modal already shown by validateBeforeCart 16104 } 16105 16106 } catch (error) { 16107 // On error, show modal and don't proceed 16108 showStockModal({
16109 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16110 is_active: false 16111 }, 'buy'); 16112 } 16113 }; 16114 16115 16116 } 16117 16118 /** 16119 * Initialize stock validation 16120 */ 16121 function init() { 16122 // Wait for DOM ready 16123 if (document.readyState === 'loading') { 16124 addEventListener(document, 'DOMContentLoaded', function() { 16125 initializeButtonAttributes(); 16126 setupVariantChangeIntegration(); // Setup variant change integration 16127 interceptCartButtons(); 16128 setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load 16129 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16130 setTimeout(() => { 16131 overrideAddToCartSubmitFunction(); 16132 overrideBuyNowFunction(); 16133 }, 100); 16134 16135 setTimeout(() => { 16136 // Re-check and re-override if pd-addtocart.js loaded later 16137 overrideAddToCartSubmitFunction(); 16138 }, 500); 16139 16140 setTimeout(() => { 16141 // Final check for very late loading scripts 16142 overrideAddToCartSubmitFunction(); 16143 }, 1000); 16144 16145 16146 }); 16147 } else { 16148 initializeButtonAttributes(); 16149 setupVariantChangeIntegration(); // Setup variant change integration 16150 interceptCartButtons(); 16151 setupGlobalClickInterceptor(); // Setup global click security interceptor 16152 16153 // Override functions after other scripts load 16154 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16155 setTimeout(() => { 16156 overrideAddToCartSubmitFunction(); 16157 overrideBuyNowFunction(); 16158 }, 100); 16159 16160 setTimeout(() => { 16161 // Re-check and re-override if pd-addtocart.js loaded later 16162 overrideAddToCartSubmitFunction(); 16163 }, 500); 16164 16165 setTimeout(() => { 16166 // Final check for very late loading scripts 16167 overrideAddToCartSubmitFunction(); 16168 }, 1000); 16169 16170 16171 } 16172 } 16173 16174 // Public API 16175 return { 16176 init: init, 16177 checkStock: checkStock, 16178 validateBeforeCart: validateBeforeCart, 16179 updateStockDisplay: updateStockDisplay, 16180 showStockModal: showStockModal, 16181 config: config 16182 }; 16183})(); 16184 16185// Auto-initialize 16186if (typeof window !== 'undefined') { 16187 StockValidator.init(); 16188} 16189 16190</script>
vendor: 2 bytes, line 16190
16190
16191<script data-inlined="pd-addtocart.js"> 16192(function(){ 16193 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16194 function getVariantSummaryText(){ 16195 const vs = document.getElementById('pd-variant-summary'); 16196 if (!vs) return ''; 16197 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16198 return items.length ? items.join(' / ') : ''; 16199 } 16200 window.addToCartSubmit = async function(e, productId){ 16201 e.preventDefault(); 16202 try { 16203 const form = e.target; 16204 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16205 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16206 await (window.addToCart ? window.addToCart(productId, qty, variantId) : Promise.reject(new Error('addToCart tidak tersedia'))); 16207 16208 if (window.PlazaAnalytics) { 16209 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16210 } 16211 16212 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16213 const sku = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim(): (window.PD_SKU||''); })(); 16214 const variantText = getVariantSummaryText(); 16215 16216 if (window.showToast) { 16217 window.showToast({ 16218 type: 'success', 16219 title: 'Berhasil ditambahkan ke Keranjang', 16220 message: `<strong>${esc(name)}</strong>${variantText?` · <em>${esc(variantText)}</em>`:''}<br>SKU: ${esc(sku)} · Qty: ${qty}`, 16221 duration: 5000, 16222 actions: [ 16223 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 16224 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 16225 ] 16226 }); 16227 } 16228 } catch (err) { 16229 if (window.showToast) { 16230 window.showToast({ type:'warn', title:'Gagal menambahkan', message: String(err?.message||'Terjadi kesalahan'), duration: 4500 }); 16231 } else { 16232 console.error(err); 16233 } 16234 } 16235 return false; 16236 }; 16237})(); 16238</script>
vendor: 2 bytes, line 16238
16238
16239<script data-inlined="pd-buynow.js"> 16240(function(){ 16241 16242 // CSRF Token Helper 16243 function getCsrfToken() { 16244 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 16245 } 16246 16247 // Utility functions 16248 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16249 16250 function getVariantSummaryText(){ 16251 const vs = document.getElementById('pd-variant-summary'); 16252 if (!vs) return ''; 16253 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16254 return items.length ? items.join(' / ') : ''; 16255 } 16256 16257 function getCurrentProductData(){ 16258 const form = document.getElementById('pd-buy-form'); 16259 if (!form) return null; 16260 16261 const productId = parseInt(form.dataset.pid || '0', 10); 16262 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16263 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16264 16265 if (!productId || productId <= 0) { 16266 throw new Error('Product ID tidak valid'); 16267 } 16268 16269 if (qty <= 0) { 16270 throw new Error('Quantity harus lebih dari 0'); 16271 } 16272 16273 return { productId, qty, variantId }; 16274 } 16275 16276 function createBuyNowPayload(productId, qty, variantId) { 16277 // Get product metadata 16278 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16279 16280 // Get correct price, SKU, and weight from selected variant data instead of DOM elements 16281 let price = 0; 16282 let sku = ''; 16283 let weight = 0; 16284 let dimensions = null; 16285 16286 if (variantId > 0 && typeof window.variants !== 'undefined' && Array.isArray(window.variants)) { 16287 // Find the selected variant in the variants array 16288 const selectedVariant = window.variants.find(v => v.id === variantId); 16289 if (selectedVariant) { 16290 price = selectedVariant.final || 0; 16291 sku = selectedVariant.sku || ''; 16292 weight = selectedVariant.weight || 0; 16293 16294 // Include dimensions if available 16295 if (selectedVariant.length_cm || selectedVariant.width_cm || selectedVariant.height_cm) { 16296 dimensions = { 16297 length_cm: selectedVariant.length_cm || 0, 16298 width_cm: selectedVariant.width_cm || 0, 16299 height_cm: selectedVariant.height_cm || 0 16300 }; 16301 } 16302 16303 // Debug info to verify fix is working 16304 if (window.console && window.console.debug) { 16305 console.debug('BuyNow: Using variant data', { 16306 variantId: variantId, 16307 price: price, 16308 sku: sku, 16309 weight: weight, 16310 dimensions: dimensions, 16311 variantData: selectedVariant 16312 }); 16313 } 16314 } 16315 } 16316 16317 // Fallback to DOM elements if variant data not found 16318 if (price === 0) { 16319 const priceTxt = document.getElementById('pd-price-current')?.textContent || ''; 16320 price = parseInt(String(priceTxt).replace(/[^\d]/g,''), 10) || 0; 16321 16322 if (window.console && window.console.debug) { 16323 console.debug('BuyNow: Using DOM price fallback', { 16324 priceTxt: priceTxt, 16325 price: price, 16326 variantId: variantId, 16327 reason: variantId > 0 ? 'variant_data_not_found' : 'no_variant_selected' 16328 }); 16329 } 16330 } 16331 16332 if (!sku) { 16333 const skuEl = document.getElementById('pd-sku'); 16334 sku = skuEl ? skuEl.textContent.trim() : (window.PD_SKU || ''); 16335 } 16336 16337 const imgEl = document.getElementById('pd-main-img'); 16338 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 16339 16340 const variantText = getVariantSummaryText(); 16341 16342 // Get pre-order data if product is pre-order 16343 const preorderData = window.PD_PREORDER_DATA || null; 16344 16345 return { 16346 product_id: productId, 16347 qty: qty, 16348 variant_id: variantId, 16349 meta: { 16350 name, 16351 sku, 16352 price, 16353 image_url, 16354 variant_text: variantText, 16355 weight: weight, 16356 dimensions: dimensions 16357 }, 16358 is_preorder: preorderData ? 'Y' : 'N', 16359 preorder_estimated_arrival: preorderData ? preorderData.estimated_arrival : null, 16360 preorder_note: preorderData ? preorderData.note : '' 16361 }; 16362 } 16363 16364 /** 16365 * Main Buy Now function
16366 * Performs validation and redirects to checkout with buy_now mode 16367 */ 16368 window.buyNow = async function(e) { 16369 if (e) e.preventDefault(); 16370 16371 try { 16372 // Check user login status 16373 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16374 document.body.classList.contains('logged-in') || 16375 window.userLoggedIn === true || 16376 document.body.dataset.userLoggedIn === 'true'; 16377 16378 if (!isLoggedIn) { 16379 // Redirect to login with return URL to current page 16380 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16381 window.location.href = '/login?return=' + currentUrl; 16382 return; 16383 } 16384 16385 // Get current product data 16386 const { productId, qty, variantId } = getCurrentProductData(); 16387 16388 // Create payload for buy now 16389 const payload = createBuyNowPayload(productId, qty, variantId); 16390 16391 // Store buy now data in sessionStorage for checkout page 16392 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16393 16394 // Track add_to_cart (buy now counts as purchase intent) 16395 if (window.PlazaAnalytics) { 16396 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16397 } 16398 16399 // Redirect to checkout with buy_now mode 16400 window.location.href = '/checkout?mode=buy_now'; 16401 16402 } catch (err) { 16403 console.error('Buy Now error:', err); 16404 16405 if (window.showToast) { 16406 window.showToast({ 16407 type: 'warn', 16408 title: 'Gagal memproses', 16409 message: String(err?.message || 'Terjadi kesalahan saat memproses pembelian'), 16410 duration: 4500 16411 }); 16412 } else { 16413 alert(err.message || 'Terjadi kesalahan saat memproses pembelian'); 16414 } 16415 } 16416 }; 16417 16418 /** 16419 * Alternative Buy Now with confirmation dialog 16420 */ 16421 window.buyNowWithConfirm = async function(e) { 16422 if (e) e.preventDefault(); 16423 16424 try { 16425 const { productId, qty, variantId } = getCurrentProductData(); 16426 16427 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16428 const variantText = getVariantSummaryText(); 16429 const priceTxt = document.getElementById('pd-price-current')?.textContent || 'Rp0'; 16430 16431 const confirmMessage = `Lanjut ke checkout?\n\nProduk: ${name}${variantText ? `\nVariant: ${variantText}` : ''}\nQty: ${qty}\nHarga: ${priceTxt}`; 16432 16433 if (!confirm(confirmMessage)) { 16434 return; 16435 } 16436 16437 // Proceed with buy now 16438 await window.buyNow(e); 16439 16440 } catch (err) { 16441 console.error('Buy Now with confirm error:', err); 16442 alert(err.message || 'Terjadi kesalahan'); 16443 } 16444 }; 16445 16446 /** 16447 * Attach event listener for Buy with Voucher button 16448 */ 16449 function setupBuyButtons() { 16450 // Setup desktop buy button (regular buy now without voucher) 16451 const buyBtn = document.getElementById('btn-buy'); 16452 if (buyBtn) { 16453 buyBtn.addEventListener('click', function(e) { 16454 e.preventDefault(); 16455 if (buyBtn.disabled) return; 16456 if (window.buyNow) { 16457 window.buyNow(e); 16458 } 16459 }); 16460 } 16461 16462 // Setup desktop buy with voucher button 16463 const voucherBtn = document.getElementById('btn-buy-voucher'); 16464 if (voucherBtn) { 16465 voucherBtn.addEventListener('click', function(e) { 16466 e.preventDefault(); 16467 16468 // Validate stock availability before proceeding 16469 if (this.disabled || this.getAttribute('data-stock-blocked') === 'true') { 16470 const stockReason = this.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16471 16472 if (window.showToast) { 16473 window.showToast({ 16474 type: 'warn', 16475 title: 'Stok Tidak Tersedia', 16476 message: stockReason, 16477 duration: 3500 16478 }); 16479 } else { 16480 alert(stockReason); 16481 } 16482 return; 16483 } 16484 16485 const voucherCode = this.dataset.voucherCode; 16486 if (voucherCode) { 16487 window.buyNowWithVoucher(e, voucherCode); 16488 } 16489 }); 16490 } 16491 16492 } 16493 16494 // CRITICAL: Attach mobile button handler at DOCUMENT level with CAPTURE phase 16495 // This is the ONLY way to guarantee it fires regardless of other scripts 16496 document.addEventListener('click', function(e) { 16497 // Check if click is on mobile buy button or its children 16498 const target = e.target.closest('#mf-buy'); 16499 if (!target) return; // Not our button 16500 16501 e.preventDefault(); 16502 e.stopPropagation(); 16503 e.stopImmediatePropagation(); 16504 16505 if (target.disabled) return; 16506
16507 // Get voucher code from parent FAB's data attribute 16508 const mobileFab = document.getElementById('pd-mobile-fab'); 16509 const voucherCode = mobileFab ? (mobileFab.getAttribute('data-voucher-code') || '') : ''; 16510 16511 if (voucherCode && voucherCode.trim() !== '' && window.buyNowWithVoucher) { 16512 window.buyNowWithVoucher(e, voucherCode); 16513 } else if (window.buyNow) { 16514 window.buyNow(e); 16515 } 16516 }, true); // CAPTURE PHASE - fires BEFORE any bubble phase handlers 16517 16518 // Execute immediately if DOM already loaded, otherwise wait 16519 if (document.readyState === 'loading') { 16520 document.addEventListener('DOMContentLoaded', setupBuyButtons); 16521 } else { 16522 setupBuyButtons(); 16523 } 16524 16525 /** 16526 * Buy Now with Auto-Applied Voucher 16527 * Applies voucher to session before redirecting to checkout (like cart does) 16528 * 16529 * @param {Event} e - Click event 16530 * @param {string} voucherCode - Voucher code to apply 16531 */ 16532 window.buyNowWithVoucher = async function(e, voucherCode) { 16533 if (e) e.preventDefault(); 16534 16535 console.log('[buyNowWithVoucher] Called with voucher code:', voucherCode); 16536 16537 try { 16538 // CRITICAL: Check stock availability first 16539 const voucherBtn = document.getElementById('btn-buy-voucher'); 16540 if (voucherBtn) { 16541 if (voucherBtn.disabled || voucherBtn.getAttribute('data-stock-blocked') === 'true') { 16542 const stockReason = voucherBtn.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16543 16544 if (window.showToast) { 16545 window.showToast({ 16546 type: 'warn', 16547 title: 'Stok Tidak Tersedia', 16548 message: stockReason, 16549 duration: 3500 16550 }); 16551 } else { 16552 alert(stockReason); 16553 } 16554 return; 16555 } 16556 } 16557 16558 // Check login status first 16559 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16560 document.body.classList.contains('logged-in') || 16561 window.userLoggedIn === true || 16562 document.body.dataset.userLoggedIn === 'true'; 16563 16564 console.log('[buyNowWithVoucher] User logged in:', isLoggedIn); 16565 16566 if (!isLoggedIn) { 16567 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16568 window.location.href = '/login?return=' + currentUrl; 16569 return; 16570 } 16571 16572 // Get current product data 16573 const { productId, qty, variantId } = getCurrentProductData(); 16574 const payload = createBuyNowPayload(productId, qty, variantId); 16575 16576 console.log('[buyNowWithVoucher] Product data:', { productId, qty, variantId }); 16577 16578 // Get category path for voucher validation 16579 const categoryPath = window.PD_CATEGORY_PATH || ''; 16580 16581 // Step 1: Clear any existing voucher first (important for clean state) 16582 console.log('[buyNowWithVoucher] Step 1: Clearing existing voucher...'); 16583 try { 16584 await fetch('/api/voucher/remove.php', { 16585 method: 'POST', 16586 headers: { 16587 'X-CSRF-Token': getCsrfToken() 16588 }, 16589 body: JSON.stringify({ 16590 csrf_token: getCsrfToken() 16591 }), 16592 credentials: 'same-origin' 16593 }); 16594 } catch (e) { 16595 // Ignore error if no voucher exists 16596 console.debug('No existing voucher to remove or removal failed'); 16597 } 16598 16599 // Step 2: Apply voucher to session (like cart does) 16600 console.log('[buyNowWithVoucher] Step 2: Applying voucher to session...'); 16601 const applyRes = await fetch('/api/voucher/apply.php', { 16602 method: 'POST', 16603 headers: { 16604 'Content-Type': 'application/json', 16605 'X-CSRF-Token': getCsrfToken() 16606 }, 16607 credentials: 'same-origin', 16608 body: JSON.stringify({ 16609 csrf_token: getCsrfToken(), 16610 code: voucherCode, 16611 items: [{ 16612 pid: productId, 16613 price: payload.meta.price, 16614 qty: qty, 16615 category_path: categoryPath 16616 }], 16617 shipping_cost: null // Will be calculated in checkout 16618 }) 16619 }); 16620 16621 const applyData = await applyRes.json(); 16622 16623 console.log('[buyNowWithVoucher] Voucher apply response:', applyData); 16624 16625 if (!applyData.success) { 16626 // Voucher invalid - show error and fallback to regular buy now 16627 console.warn('[buyNowWithVoucher] Voucher application failed:', applyData.message); 16628 if (window.showToast) { 16629 window.showToast({ 16630 type: 'error', 16631 title: 'Voucher Tidak Valid', 16632 message: applyData.message || 'Voucher tidak dapat diterapkan untuk produk ini', 16633 duration: 4000 16634 }); 16635 } else { 16636 alert(applyData.message || 'Voucher tidak dapat diterapkan'); 16637 } 16638 16639 // Fallback to regular buy now without voucher 16640 console.log('Voucher failed, proceeding with regular buy now'); 16641 return window.buyNow(e); 16642 } 16643 16644 // Step 3: Store buy now data in sessionStorage 16645 console.log('[buyNowWithVoucher] Step 3: Storing buy now data in sessionStorage'); 16646 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16647 16648 // Step 4: Redirect to checkout 16649 // Voucher sudah di session, checkout akan auto-load via loadAppliedVoucher() 16650 console.log('[buyNowWithVoucher] Step 4: Redirecting to checkout with voucher applied'); 16651 window.location.href = '/checkout?mode=buy_now'; 16652 16653 } catch (err) { 16654 console.error('Buy Now with Voucher error:', err); 16655 16656 if (window.showToast) { 16657 window.showToast({ 16658 type: 'error', 16659 title: 'Terjadi Kesalahan', 16660 message: 'Gagal memproses pembelian dengan voucher. Silakan coba lagi.', 16661 duration: 3000 16662 }); 16663 } else { 16664 alert('Gagal memproses pembelian. Silakan coba lagi.'); 16665 } 16666 } 16667 }; 16668})(); 16669</script>
vendor: 2 bytes, line 16669
16669
16670<script data-inlined="pd-voucher-recalc.js"> 16671/** 16672 * Product Detail - Voucher Discount Recalculation 16673 * 16674 * Handles dynamic voucher discount recalculation when variant price changes 16675 * Supports both PERCENTAGE and FIXED_AMOUNT voucher types 16676 */ 16677 16678(function() { 16679 'use strict'; 16680 16681 /** 16682 * Calculate voucher discount based on type and price 16683 * 16684 * @param {string} type - Voucher type: 'PERCENTAGE' or 'FIXED_AMOUNT' 16685 * @param {number} value - Voucher value (percentage or fixed amount) 16686 * @param {number} price - Current variant price 16687 * @param {number} maxDiscount - Maximum discount for percentage vouchers (0 = no limit) 16688 * @return {number} Calculated discount amount 16689 */ 16690 function calculateVoucherDiscount(type, value, price, maxDiscount) { 16691 if (type === 'PERCENTAGE') { 16692 // Calculate percentage discount 16693 let discount = (price * value) / 100; 16694 16695 // Apply max discount cap if set 16696 if (maxDiscount > 0 && discount > maxDiscount) { 16697 discount = maxDiscount; 16698 } 16699 16700 return Math.round(discount); 16701 } else { 16702 // FIXED_AMOUNT - discount doesn't change with price 16703 return Math.round(value); 16704 } 16705 } 16706 16707 /** 16708 * Format number as Rupiah without 'Rp' prefix 16709 * 16710 * @param {number} num - Number to format 16711 * @return {string} Formatted number (e.g., "20.000") 16712 */ 16713 function formatNumber(num) { 16714 return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 16715 } 16716 16717 /** 16718 * Update voucher button discount label 16719 * 16720 * @param {number} newPrice - New variant price 16721 */ 16722 function updateVoucherDiscount(newPrice) { 16723 const voucherBtn = document.getElementById('btn-buy-voucher'); 16724 if (!voucherBtn) return; 16725 16726 // Get voucher data from button attributes 16727 const voucherType = voucherBtn.dataset.voucherType; 16728 const voucherValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 16729 const voucherMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 16730 16731 if (!voucherType || voucherValue === 0) return; 16732 16733 // Calculate new discount 16734 const newDiscount = calculateVoucherDiscount(voucherType, voucherValue, newPrice, voucherMax); 16735 16736 // Update the discount label 16737 const saveLabel = voucherBtn.querySelector('.bv-save'); 16738 if (saveLabel) { 16739 saveLabel.textContent = '-Rp' + formatNumber(newDiscount); 16740 } 16741 16742 // Also update mobile FAB voucher display if exists 16743 updateMobileFabVoucher(newDiscount); 16744 } 16745 16746 /** 16747 * Update mobile FAB voucher value 16748 * 16749 * @param {number} discountAmount - New discount amount 16750 */ 16751 function updateMobileFabVoucher(discountAmount) { 16752 const fab = document.getElementById('pd-mobile-fab'); 16753 if (fab) { 16754 fab.dataset.voucherValue = discountAmount.toString(); 16755 } 16756 } 16757 16758 /** 16759 * Listen to variant change events and recalculate discount 16760 */ 16761 function initVoucherRecalculation() { 16762 window.addEventListener('pd-variant-changed', function(e) { 16763 const detail = e.detail || {}; 16764 const newPrice = detail.price || 0; 16765 16766 if (newPrice > 0) { 16767 updateVoucherDiscount(newPrice); 16768 } 16769 }); 16770 } 16771 16772 // Initialize when DOM is ready 16773 if (document.readyState === 'loading') { 16774 document.addEventListener('DOMContentLoaded', initVoucherRecalculation); 16775 } else { 16776 initVoucherRecalculation(); 16777 } 16778 16779})(); 16780 16781</script>
vendor: 2 bytes, line 16781
16781
16782<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>
16782 16783
16784<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16785(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'}})}} 16786 16787function _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()}})(); 16788 16789(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()}})})(); 16790</script>
16790<footer class="site-footer footer-compact variant-stripes" role="contentinfo"> 16791 <div class="ft-top ft-collapsible" id="ftTop"> 16792 <div class="container ft-grid"> 16793 16794 <div class="ft-col ft-help"> 16795 <h4 class="ft-title">Butuh Bantuan?</h4> 16796 <p class="ft-desc">Hubungi kami melalui Official WhatsApp interaktif sekarang.</p> 16797 <a class="ft-wa-btn" href="https://pastikale.me/wamak" target="_blank" rel="nofollow noopener"> 16798 <span class="ico" aria-hidden="true"> 16799 <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> 16800 </span> 16801 Kirim Pesan 16802 </a> 16803 16804 <p class="ft-desc sm-gap">Ikuti kami:</p> 16805 <div class="ft-social"> 16806 <a aria-label="Facebook" href="https://facebook.com/PlazaIt/"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M13.5 22v-8.2h2.7l.4-3.2h-3.1V8c0-.9.3-1.5 1.6-1.5H17V3.7c-.3 0-1.2-.1-2.2-.1-2.2 0-3.8 1.3-3.8 3.9v2.2H8v3.2h3v8.1h2.5Z"/></svg></a> 16807 <a aria-label="Instagram" href="https://www.instagram.com/plazait"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M7.8 2h8.4A5.8 5.8 0 0 1 22 7.8v8.4A5.8 5.8 0 0 1 16.2 22H7.8A5.8 5.8 0 0 1 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2Zm0 2A3.8 3.8 0 0 0 4 7.8v8.4A3.8 3.8 0 0 0 7.8 20h8.4a3.8 3.8 0 0 0 3.8-3.8
16807V7.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> 16808 <a aria-label="TikTok" href="https://www.tiktok.com/@plazait"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M18.9 7.4a6.4 6.4 0 0 1-3.2-1.2v6.3a5.47 5.47 0 1 1-5.5-5.5c.2 0 .4 0 .6.02v2.37a2.92 2.92 0 0 0-.6-.07 2.8 2.8 0 1 0 2.8 2.8V2h2.3a4.6 4.6 0 0 0 4.5 4.1v2.3Z"/></svg></a> 16809 </div> 16810 </div> 16811 16812 <!-- Collapsible Content Wrapper (Non-Homepage Only) --> 16813 <div class="ft-collapsible-content" id="ftCollapsible"> 16814 16815 <div class="ft-col"> 16816 <h4 class="ft-title">Tentang EDUTOTO</h4> 16817 <ul class="ft-links"> 16818 <li><a href="https://norita.mx/">Tentang EDUTOTO</a></li> 16819 <li><a href="https://norita.mx/">Kontak EDUTOTO</a></li> 16820 <li><a href="https://norita.mx/">Lokasi Toko</a></li> 16821 <li><a href="https://norita.mx/">FAQ</a></li> 16822 </ul> 16823 <div class="ft-address"> 16824 <p class="company">PT. Edutama Relaksasi Nusantara</p> 16825 <p>Jl. Prof. Dr. Satrio Kav. 3-5, Kel. Karet Semanggi,</p> 16826 <p>Kec. Setiabudi, Kota Jakarta Selatan, DKI Jakarta, 12930</p> 16827 </div> 16828 </div> 16829 16830 <div class="ft-col"> 16831 <h4 class="ft-title">Informasi</h4> 16832 <ul class="ft-links"> 16833 <li><a href="/syarat-dan-ketentuan">Syarat & Ketentuan</a></li> 16834 <li><a href="/kebijakan-privasi">Kebijakan Privasi</a></li> 16835 <li><a href="/kebijakan-cookie">Kebijakan Cookie</a></li> 16836 <li><a href="/pengiriman-dan-pengembalian">Pengiriman & Pengembalian</a></li> 16837 <li><a href="/pembayaran-dan-cicilan">Pembayaran & Cicilan</a></li> 16838 <li><a href="/garansi-dan-layanan">Garansi & Layanan</a></li> 16839 </ul> 16840 16841 <div class="ft-payments payments-small" aria-label="Metode Pembayaran"> 16842 <div class="pay-row"> 16843 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-mandiri.png" alt="Mandiri" loading="lazy"> 16844 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bni.gif" alt="BNI" loading="lazy"> 16845 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bri.gif" alt="BRI" loading="lazy"> 16846 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-qris.gif" alt="QRIS" loading="lazy"> 16847 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bca.gif" alt="BCA" loading="lazy"> 16848 </div> 16849 </div> 16850 </div> 16851 16852 </div> 16853 <!-- End Collapsible Content --> 16854 16855 <!-- Expand Button (Mobile Only) --> 16856 <button class="ft-expand-btn" id="ftExpandBtn" aria-label="Lihat semua informasi footer"> 16857 <span class="expand-text">Selengkapnya</span> 16858 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 16859 <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> 16860 </svg> 16861 </button> 16862 16863 </div> 16864 </div> 16865 <div class="ft-bottom"> 16866 <div class="container ft-bottom-inner"> 16867 <div class="ft-copy"> 16868 <span>ANGIN-RIBUT © <span id="yearNow">2026</span></span> 16869 <span>PT ANGIN-RIBUT. All rights reserved.</span> 16870 </div> 16871 </div> 16872 </div> 16873 16874 <style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16875 /* Chat Widget Styles - Lightweight & Minimal */ 16876 .chat-widget { 16877 position: fixed; 16878 bottom: 20px; 16879 right: 20px; 16880 z-index: 9999; 16881 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 16882 } 16883 16884 /* Hide floating button on mobile - use bottom nav instead */ 16885 @media (max-width: 640px) { 16886 .chat-toggle-btn { 16887 display: none !important; 16888 } 16889 16890 /* Ensure popup is still visible on mobile even when button is hidden */ 16891 .chat-widget { 16892 display: block !important; 16893 } 16894 16895 .chat-popup { 16896 display: none; /* Hidden by default, shown via JS */ 16897 } 16898 16899 .chat-popup[style*="display: block"], 16900 .chat-popup[style*="display:block"] { 16901 display: block !important; 16902 } 16903 } 16904 16905 .chat-toggle-btn { 16906 width: 60px; 16907 height: 60px; 16908 border-radius: 50%; 16909 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 16910 border: none; 16911 color: white; 16912 cursor: pointer; 16913 box-shadow: 0 4px 12px rgba(201, 162, 39, 0.3); 16914 position: relative; 16915 transition: transform 0.2s, box-shadow 0.2s; 16916 } 16917 16918 .chat-toggle-btn:hover { 16919 transform: scale(1.05); 16920 box-shadow: 0 6px 20px rgba(201, 162, 39, 0.4); 16921 } 16922 16923 #chat-toggle-btn .chat-icon { 16924 position: absolute; 16925 top: 50%; 16926 left: 50%; 16927 transform: translate(-50%, -50%); 16928 width: 28px !important; 16929 height: 28px !important; 16930 } 16931 16932 .chat-unread-badge { 16933 position: absolute; 16934 top: -5px; 16935 right: -5px; 16936 background: #ef4444; 16937 color: white; 16938 border-radius: 12px; 16939 padding: 2px 6px; 16940 font-size: 11px; 16941 font-weight: 600;
16942 min-width: 20px; 16943 text-align: center; 16944 } 16945 16946 .chat-popup { 16947 position: absolute; 16948 bottom: 80px; 16949 right: 0; 16950 width: 380px; 16951 max-width: calc(100vw - 40px); 16952 height: 500px; 16953 max-height: calc(100vh - 120px); 16954 background: white; 16955 border-radius: 12px; 16956 box-shadow: 0 10px 40px rgba(0,0,0,0.2); 16957 display: flex; 16958 flex-direction: column; 16959 overflow: hidden; 16960 } 16961 16962 .chat-popup-header { 16963 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 16964 color: white; 16965 padding: 16px 20px; 16966 display: flex; 16967 justify-content: space-between; 16968 align-items: center; 16969 flex-shrink: 0; 16970 } 16971 16972 .chat-header-title { 16973 display: flex; 16974 flex-direction: column; 16975 gap: 2px; 16976 min-height: 0; 16977 } 16978 16979 .chat-popup-header h3 { 16980 margin: 0; 16981 font-size: 18px; 16982 font-weight: 600; 16983 line-height: 1.2; 16984 } 16985 16986 .chat-operating-hours { 16987 font-size: 11px; 16988 opacity: 0.9; 16989 font-weight: 400; 16990 display: flex; 16991 align-items: center; 16992 gap: 4px; 16993 line-height: 1.2; 16994 } 16995 16996 .chat-operating-hours svg { 16997 width: 12px; 16998 height: 12px; 16999 opacity: 0.8; 17000 flex-shrink: 0; 17001 } 17002 17003 .chat-header-actions { 17004 display: flex; 17005 align-items: center; 17006 gap: 8px; 17007 } 17008 17009 /* Notification Toggle Button */ 17010 .btn-notif-toggle { 17011 background: none; 17012 border: none; 17013 color: white; 17014 font-size: 18px; 17015 cursor: pointer; 17016 padding: 4px 8px; 17017 transition: all 0.2s; 17018 opacity: 0.8; 17019 } 17020 17021 .btn-notif-toggle:hover { 17022 opacity: 1; 17023 transform: scale(1.1); 17024 } 17025 17026 /* Icon states */ 17027 .btn-notif-toggle[data-status="not-subscribed"] svg { 17028 opacity: 0.7; 17029 } 17030 17031 .btn-notif-toggle[data-status="not-subscribed"]:hover svg { 17032 opacity: 1; 17033 } 17034 17035 .btn-notif-toggle[data-status="subscribed"] svg { 17036 opacity: 1; 17037 animation: bellRing 2s ease-in-out; 17038 } 17039 17040 .btn-notif-toggle[data-status="blocked"] svg { 17041 color: #ff6b6b; 17042 } 17043 17044 .btn-notif-toggle[data-status="not-supported"] { 17045 display: none !important; 17046 } 17047 17048 @keyframes bellRing { 17049 0%, 100% { transform: rotate(0deg); } 17050 10%, 30% { transform: rotate(-10deg); } 17051 20%, 40% { transform: rotate(10deg); } 17052 } 17053 17054 /* Notification Info Bar */ 17055 .chat-notif-info-bar { 17056 position: absolute; 17057 top: 64px; 17058 left: 0; 17059 right: 0; 17060 background: linear-gradient(to right, #e3f2fd, #f5f5f5); 17061 border-bottom: 1px solid #bbdefb; 17062 padding: 8px 12px; 17063 display: flex; 17064 align-items: center; 17065 gap: 8px; 17066 font-size: 12px; 17067 color: #616161; 17068 z-index: 10; 17069 animation: slideDown 0.3s ease; 17070 } 17071 17072 .chat-notif-info-bar svg { 17073 flex-shrink: 0; 17074 } 17075 17076 .chat-notif-info-bar .small { 17077 flex: 1; 17078 line-height: 1.4; 17079 } 17080 17081 .chat-notif-info-bar .btn-dismiss { 17082 background: none; 17083 border: none; 17084 color: #9e9e9e; 17085 cursor: pointer; 17086 padding: 0 4px; 17087 font-size: 16px; 17088 transition: color 0.2s; 17089 } 17090 17091 .chat-notif-info-bar .btn-dismiss:hover { 17092 color: #616161; 17093 } 17094 17095 @keyframes slideDown { 17096 from { 17097 max-height: 0; 17098 opacity: 0; 17099 } 17100 to { 17101 max-height: 50px; 17102 opacity: 1;
17103 } 17104 } 17105 17106 .chat-close-btn { 17107 background: none; 17108 border: none; 17109 color: white; 17110 font-size: 28px; 17111 line-height: 1; 17112 cursor: pointer; 17113 padding: 0; 17114 width: 30px; 17115 height: 30px; 17116 } 17117 17118 .chat-popup-body { 17119 flex: 1 1 auto; 17120 display: flex; 17121 flex-direction: column; 17122 min-height: 0; 17123 overflow: hidden; 17124 height: calc(100% - 64px); 17125 position: relative; 17126 transition: padding-top 0.3s ease; 17127 } 17128 17129 /* Add padding when info bar is visible */ 17130 .chat-popup-body.has-info-bar { 17131 padding-top: 40px; 17132 } 17133 17134 .chat-view { 17135 flex: 1 1 auto; 17136 display: none; 17137 flex-direction: column; 17138 min-height: 0; 17139 overflow: hidden; 17140 } 17141 17142 #chat-list-view { 17143 display: flex; 17144 } 17145 17146 #chat-messages-view { 17147 display: none; 17148 } 17149 17150 #chat-messages-view.active { 17151 display: flex; 17152 flex-direction: column; 17153 } 17154 17155 #chat-new-view { 17156 display: none; 17157 } 17158 17159 #chat-new-view.active { 17160 display: flex; 17161 } 17162 17163 .chat-conversations-list { 17164 flex: 1; 17165 overflow-y: auto; 17166 padding: 0; 17167 background: white; 17168 } 17169 17170 /* Custom Scrollbar for Conversations List */ 17171 .chat-conversations-list::-webkit-scrollbar { 17172 width: 6px; 17173 } 17174 17175 .chat-conversations-list::-webkit-scrollbar-track { 17176 background: #f1f1f1; 17177 } 17178 17179 .chat-conversations-list::-webkit-scrollbar-thumb { 17180 background: #C9A227; 17181 border-radius: 3px; 17182 } 17183 17184 .chat-conversations-list::-webkit-scrollbar-thumb:hover { 17185 background: #8B6914; 17186 } 17187 17188 /* Firefox */ 17189 .chat-conversations-list { 17190 scrollbar-width: thin; 17191 scrollbar-color: #C9A227 #f1f1f1; 17192 } 17193 17194 .chat-conversation-item { 17195 padding: 12px 16px; 17196 cursor: pointer; 17197 display: flex; 17198 gap: 12px; 17199 align-items: flex-start; 17200 transition: background 0.15s ease; 17201 color: #33371f; 17202 border-bottom: 1px solid #f0f0f0; 17203 } 17204 17205 .chat-conversation-item:hover { 17206 background: #f5f5f5; 17207 } 17208 17209 .chat-conversation-item.active { 17210 background: #FFF5EB; 17211 } 17212 17213 /* WhatsApp-like Avatar */ 17214 .conversation-avatar { 17215 width: 48px; 17216 height: 48px; 17217 border-radius: 50%; 17218 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 17219 flex-shrink: 0; 17220 display: flex; 17221 align-items: center; 17222 justify-content: center; 17223 color: white; 17224 font-weight: 600; 17225 font-size: 18px; 17226 } 17227 17228 .conversation-avatar svg { 17229 width: 24px; 17230 height: 24px; 17231 } 17232 17233 /* WhatsApp-like Content */ 17234 .conversation-content { 17235 flex: 1; 17236 min-width: 0; 17237 display: flex; 17238 flex-direction: column; 17239 gap: 4px; 17240 } 17241 17242 .conversation-header { 17243 display: flex; 17244 justify-content: space-between; 17245 align-items: center; 17246 gap: 8px; 17247 } 17248 17249 .conversation-name { 17250 font-weight: 600; 17251 font-size: 15px; 17252 color: #111; 17253 white-space: nowrap; 17254 overflow: hidden; 17255 text-overflow: ellipsis; 17256 } 17257 17258 .conversation-time { 17259 font-size: 12px; 17260 color: #7d8166; 17261 flex-shrink: 0; 17262 } 17263 17264 .conversation-preview { 17265 display: flex; 17266 justify-content: space-between; 17267 align-items: center; 17268 gap: 8px; 17269 } 17270 17271 .conversation-message { 17272 font-size: 14px; 17273 color: #667781; 17274 white-space: nowrap; 17275 overflow: hidden; 17276 text-overflow: ellipsis; 17277 margin: 0; 17278 flex: 1; 17279 } 17280 17281 .conversation-unread { 17282 background: #25D366; 17283 color: white; 17284 border-radius: 12px; 17285 padding: 2px 8px; 17286 font-size: 12px; 17287 font-weight: 600;
17288 min-width: 20px; 17289 text-align: center; 17290 flex-shrink: 0; 17291 } 17292 17293 /* Active item styling */ 17294 .chat-conversation-item.active .conversation-name { 17295 color: #C9A227; 17296 } 17297 17298 .chat-conversation-item.active .conversation-time { 17299 color: #C9A227; 17300 } 17301 17302 .chat-new-btn { 17303 margin: 12px; 17304 padding: 12px; 17305 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 17306 color: white; 17307 border: none; 17308 border-radius: 8px; 17309 cursor: pointer; 17310 font-weight: 500; 17311 font-size: 14px; 17312 display: flex; 17313 align-items: center; 17314 justify-content: center; 17315 gap: 8px; 17316 transition: all 0.2s; 17317 flex-shrink: 0; 17318 } 17319 17320 .chat-new-btn:hover { 17321 transform: translateY(-1px); 17322 box-shadow: 0 4px 8px rgba(201, 162, 39, 0.3); 17323 } 17324 17325 .chat-conversation-title { 17326 flex: 1; 17327 font-weight: 600; 17328 font-size: 16px; 17329 color: #1f2937; 17330 } 17331 17332 .chat-messages-container { 17333 flex: 1; 17334 overflow-y: auto; 17335 padding: 16px; 17336 display: flex; 17337 flex-direction: column; 17338 gap: 8px; 17339 min-height: 0; 17340 background: #E5DDD5; 17341 } 17342 17343 /* Custom Scrollbar for Messages Container */ 17344 .chat-messages-container::-webkit-scrollbar { 17345 width: 6px; 17346 } 17347 17348 .chat-messages-container::-webkit-scrollbar-track { 17349 background: rgba(0, 0, 0, 0.05); 17350 } 17351 17352 .chat-messages-container::-webkit-scrollbar-thumb { 17353 background: rgba(201, 162, 39, 0.6); 17354 border-radius: 3px; 17355 } 17356 17357 .chat-messages-container::-webkit-scrollbar-thumb:hover { 17358 background: #C9A227; 17359 } 17360 17361 /* Firefox */ 17362 .chat-messages-container { 17363 scrollbar-width: thin; 17364 scrollbar-color: rgba(201, 162, 39, 0.6) rgba(0, 0, 0, 0.05); 17365 } 17366 17367 /* WhatsApp Group-like Messages */ 17368 .chat-message { 17369 display: flex; 17370 margin-bottom: 4px; 17371 animation: fadeIn 0.2s ease; 17372 } 17373 17374 @keyframes fadeIn { 17375 from { opacity: 0; transform: translateY(10px); } 17376 to { opacity: 1; transform: translateY(0); } 17377 } 17378 17379 .chat-message.mine { 17380 justify-content: flex-end; 17381 } 17382 17383 .chat-message-bubble { 17384 max-width: 80%; 17385 padding: 6px 8px 8px 10px; 17386 border-radius: 8px; 17387 position: relative; 17388 box-shadow: 0 1px 0.5px rgba(0,0,0,0.13); 17389 } 17390 17391 /* Bubble from others (Admin/CS) */ 17392 .chat-message:not(.mine) .chat-message-bubble { 17393 background: #FFFFFF; 17394 border-radius: 0 8px 8px 8px; 17395 } 17396 17397 /* Bubble tail for others */ 17398 .chat-message:not(.mine) .chat-message-bubble::before { 17399 content: ''; 17400 position: absolute; 17401 top: 0; 17402 left: -8px; 17403 width: 0; 17404 height: 0; 17405 border-style: solid; 17406 border-width: 0 8px 13px 0; 17407 border-color: transparent #FFFFFF transparent transparent; 17408 } 17409 17410 /* Bubble from user (mine) */ 17411 .chat-message.mine .chat-message-bubble { 17412 background: #e4f5d0; /* Light blue for user messages */ 17413 border-radius: 8px 0 8px 8px; 17414 } 17415 17416 /* Bubble tail for mine */ 17417 .chat-message.mine .chat-message-bubble::before { 17418 content: ''; 17419 position: absolute; 17420 top: 0; 17421 right: -8px; 17422 width: 0; 17423 height: 0; 17424 border-style: solid; 17425 border-width: 0 0 13px 8px; 17426 border-color: transparent transparent transparent #eaf5d0; 17427 } 17428 17429 .message-sender { 17430 font-size: 12.5px; 17431 font-weight: 600; 17432 color: #C9A227 !important; 17433 margin-bottom: 4px;
17434 } 17435 17436 .chat-message.mine .message-sender { 17437 display: none; 17438 } 17439 17440 /* Message text */ 17441 .message-text { 17442 font-size: 14px; 17443 line-height: 19px; 17444 color: #ffffff !important; 17445 word-wrap: break-word; 17446 white-space: pre-wrap; 17447 margin-bottom: 4px; 17448 } 17449 17450 /* Message metadata (time + status) */ 17451 .message-meta { 17452 display: flex; 17453 align-items: center; 17454 justify-content: flex-end; 17455 gap: 4px; 17456 margin-top: 2px; 17457 } 17458 17459 .message-time { 17460 font-size: 11px; 17461 color: #728166 !important; 17462 line-height: 15px; 17463 } 17464 17465 /* Use multiple selectors to override global styles */ 17466 .chat-message-bubble .message-status, 17467 .message-meta .message-status, 17468 .chat-message .message-status { 17469 width: 16px !important; 17470 height: 16px !important; 17471 min-width: 16px !important; 17472 min-height: 16px !important; 17473 max-width: 16px !important; 17474 max-height: 16px !important; 17475 flex-shrink: 0; 17476 color: #728166 !important; 17477 fill: currentColor !important; 17478 } 17479 17480 .chat-message:not(.mine) .message-status { 17481 display: none; 17482 } 17483 17484 /* Loading dots animation */ 17485 @keyframes loadingDots { 17486 0%, 20% { content: '.'; } 17487 40% { content: '..'; } 17488 60%, 100% { content: '...'; } 17489 } 17490 17491 .message-status.loading::after { 17492 content: '...'; 17493 animation: loadingDots 1.5s infinite; 17494 font-size: 16px; 17495 line-height: 16px; 17496 } 17497 17498 .message-status.loading path { 17499 display: none; 17500 } 17501 17502 .message-status.failed { 17503 color: #ef4444 !important; 17504 cursor: pointer; 17505 } 17506 17507 .message-status.sent { 17508 color: #667781 !important; 17509 } 17510 17511 .chat-new-form { 17512 flex: 1; 17513 overflow-y: auto; 17514 padding: 16px; 17515 display: flex; 17516 flex-direction: column; 17517 } 17518 17519 .chat-new-form form { 17520 display: flex; 17521 flex-direction: column; 17522 gap: 16px; 17523 flex: 1; 17524 } 17525 17526 .chat-new-form .form-group { 17527 display: flex; 17528 flex-direction: column; 17529 gap: 6px; 17530 } 17531 17532 .chat-new-form label { 17533 font-size: 14px; 17534 font-weight: 500; 17535 color: #374151; 17536 } 17537 17538 .chat-new-form .form-control { 17539 width: 100%; 17540 padding: 10px 12px; 17541 border: 1px solid #d1d5db; 17542 border-radius: 8px; 17543 font-size: 14px; 17544 font-family: inherit; 17545 transition: border-color 0.2s; 17546 box-sizing: border-box; 17547 } 17548 17549 .chat-new-form .form-control:focus { 17550 outline: none; 17551 border-color: #C9A227; 17552 box-shadow: 0 0 0 3px rgba(201, 162, 39, 0.1); 17553 } 17554 17555 .chat-new-form textarea.form-control { 17556 resize: vertical; 17557 min-height: 100px; 17558 } 17559 17560 .chat-new-form .btn { 17561 padding: 12px 20px; 17562 border: none; 17563 border-radius: 8px; 17564 font-size: 15px; 17565 font-weight: 500; 17566 cursor: pointer; 17567 transition: all 0.2s; 17568 text-align: center; 17569 } 17570 17571 .chat-new-form .btn-primary { 17572 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 17573 color: white; 17574 } 17575 17576 .chat-new-form .btn-primary:hover { 17577 transform: translateY(-1px); 17578 box-shadow: 0 4px 8px rgba(201, 162, 39, 0.3); 17579 } 17580 17581 .chat-new-form .btn-primary:active { 17582 transform: scale(0.98); 17583 } 17584 17585 .chat-new-form .btn-block { 17586 width: 100%; 17587 margin-top: auto; 17588 } 17589 17590 .chat-message-form { 17591 padding: 12px; 17592 border-top: 1px solid #e5e7eb; 17593 display: flex; 17594 flex-wrap: wrap; 17595 gap: 8px; 17596 align-items: flex-end; 17597 flex-shrink: 0; 17598 background: white; 17599 } 17600 17601 .chat-product-card { 17602 flex: 1 1 100%; 17603 max-width: 100%; 17604 display: flex; 17605 gap: 12px; 17606 padding: 12px; 17607 background: #f3f4f6; 17608 border-radius: 8px; 17609 margin-bottom: 4px;
17610 border: 1px solid #e5e7eb; 17611 } 17612 17613 .chat-product-card img { 17614 width: 60px; 17615 height: 60px; 17616 object-fit: cover; 17617 border-radius: 6px; 17618 flex-shrink: 0; 17619 } 17620 17621 .chat-product-card-content { 17622 flex: 1; 17623 min-width: 0; 17624 } 17625 17626 .chat-product-card-name { 17627 font-weight: 500; 17628 font-size: 14px; 17629 color: #1f2937; 17630 margin-bottom: 4px; 17631 white-space: nowrap; 17632 overflow: hidden; 17633 text-overflow: ellipsis; 17634 } 17635 17636 .chat-product-card-variant { 17637 font-size: 12px; 17638 color: #74806b; 17639 margin-bottom: 4px; 17640 } 17641 17642 .chat-product-card-price { 17643 font-size: 13px; 17644 color: #8bea66; 17645 font-weight: 600; 17646 } 17647 17648 .chat-product-card-remove { 17649 align-self: flex-start; 17650 background: none; 17651 border: none; 17652 color: #9ca3af; 17653 cursor: pointer; 17654 padding: 4px; 17655 font-size: 18px; 17656 line-height: 1; 17657 transition: color 0.2s; 17658 } 17659 17660 .chat-product-card-remove:hover { 17661 color: #ef4444; 17662 } 17663 17664 /* Product card in messages (read-only display) */ 17665 .message-product-card { 17666 display: flex; 17667 gap: 10px; 17668 padding: 10px; 17669 background: rgba(255, 255, 255, 0.5); 17670 border-radius: 8px; 17671 margin-bottom: 6px; 17672 border: 1px solid rgba(0, 0, 0, 0.08); 17673 align-items: center; 17674 } 17675 17676 .chat-message.mine .message-product-card { 17677 background: rgba(255, 255, 255, 0.7); 17678 } 17679 17680 .message-product-card img { 17681 width: 50px; 17682 height: 50px; 17683 object-fit: cover; 17684 border-radius: 6px; 17685 flex-shrink: 0; 17686 } 17687 17688 .message-product-info { 17689 flex: 1; 17690 min-width: 0; 17691 } 17692 17693 .message-product-name { 17694 font-weight: 500; 17695 font-size: 13px; 17696 color: #33371f; 17697 margin-bottom: 2px; 17698 overflow: hidden; 17699 text-overflow: ellipsis; 17700 display: -webkit-box; 17701 -webkit-line-clamp: 2; 17702 -webkit-box-orient: vertical; 17703 line-height: 1.3; 17704 } 17705 17706 .message-product-variant { 17707 font-size: 11px; 17708 color: #78806b; 17709 margin-bottom: 2px; 17710 } 17711 17712 .message-product-price { 17713 font-size: 12px; 17714 color: #c7ea66; 17715 font-weight: 600; 17716 } 17717 17718 .message-product-link { 17719 flex-shrink: 0; 17720 width: 32px; 17721 height: 32px; 17722 display: flex; 17723 align-items: center; 17724 justify-content: center; 17725 background: rgba(102, 126, 234, 0.1); 17726 border-radius: 6px; 17727 color: #c7ea66; 17728 text-decoration: none; 17729 transition: all 0.2s; 17730 } 17731 17732 .message-product-link:hover { 17733 background: #b7ea66; 17734 color: white; 17735 } 17736 17737 .message-product-link svg { 17738 width: 16px; 17739 height: 16px; 17740 } 17741 17742 .chat-message-input { 17743 flex: 1; 17744 border: 1px solid #d1d5db; 17745 border-radius: 20px; 17746 padding: 10px 16px; 17747 resize: none; 17748 max-height: 100px; 17749 font-family: inherit; 17750 font-size: 14px; 17751 line-height: 1.5; 17752 box-sizing: border-box; 17753 overflow-y: hidden; 17754 } 17755 17756 .chat-message-input:focus { 17757 outline: none; 17758 border-color: #C9A227;
17759 box-shadow: 0 0 0 3px rgba(201, 162, 39, 0.1); 17760 } 17761 17762 .chat-send-btn { 17763 width: 40px; 17764 height: 40px; 17765 border-radius: 50%; 17766 background: linear-gradient(135deg, #C9A227 0%, #8B6914 100%); 17767 border: none; 17768 color: white; 17769 cursor: pointer; 17770 flex-shrink: 0; 17771 display: flex; 17772 align-items: center; 17773 justify-content: center; 17774 transition: all 0.2s; 17775 } 17776 17777 .chat-send-btn:hover { 17778 transform: scale(1.05); 17779 box-shadow: 0 2px 8px rgba(201, 162, 39, 0.3); 17780 } 17781 17782 .chat-send-btn:active { 17783 transform: scale(0.95); 17784 } 17785 17786 .chat-loading { 17787 text-align: center; 17788 padding: 20px; 17789 color: #6b7280; 17790 } 17791 17792 /* Date Separator (WhatsApp-style) */ 17793 .date-separator { 17794 display: flex; 17795 align-items: center; 17796 gap: 12px; 17797 margin: 24px 0 16px; 17798 opacity: 0; 17799 animation: fadeInSeparator 0.3s ease forwards; 17800 } 17801 17802 @keyframes fadeInSeparator { 17803 from { 17804 opacity: 0; 17805 transform: translateY(-10px); 17806 } 17807 to { 17808 opacity: 1; 17809 transform: translateY(0); 17810 } 17811 } 17812 17813 .date-separator-line { 17814 flex: 1; 17815 height: 1px; 17816 background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1) 50%, transparent); 17817 } 17818 17819 .date-separator-text { 17820 flex-shrink: 0; 17821 padding: 4px 12px; 17822 background: #e9ecef; 17823 color: #6c757d; 17824 font-size: 11px; 17825 font-weight: 600; 17826 text-transform: uppercase; 17827 letter-spacing: 0.5px; 17828 border-radius: 12px; 17829 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 17830 } 17831 17832 /* Mobile responsive */ 17833 @media (max-width: 640px) { 17834 .chat-popup { 17835 position: fixed !important; /* Override absolute positioning */ 17836 bottom: 45px !important; /* Above bottom nav */ 17837 right: 0 !important; 17838 left: 0 !important; 17839 width: 100% !important; 17840 max-width: 100% !important; 17841 /* Use dvh (dynamic viewport height) for browser UI awareness */ 17842 height: calc(100dvh - 45px) !important; 17843 max-height: calc(100dvh - 45px) !important; 17844 /* Fallback for browsers that don't support dvh */ 17845 height: calc(var(--viewport-height, 100vh) - 45px) !important; 17846 max-height: calc(var(--viewport-height, 100vh) - 45px) !important; 17847 border-radius: 0 !important; 17848 z-index: 9999 !important; 17849 } 17850 17851 .chat-popup-header { 17852 border-radius: 0; 17853 } 17854 17855 .chat-messages-container, 17856 .chat-conversations-list { 17857 /* Adjust for mobile keyboard using dynamic viewport */ 17858 max-height: calc(100dvh - 190px); 17859 /* Fallback for browsers that don't support dvh */ 17860 max-height: calc(var(--viewport-height, 100vh) - 190px); 17861 } 17862 } 17863 </style> 17864 <!-- Chat Widget Button --> 17865 <div id="chat-widget" class="chat-widget" 17866 data-user-id="" 17867 data-is-logged-in="false" 17868 data-csrf-token="df5d776748b1ed8c8dd0d987bae99b1ee3dea6c6dcbe0eedb1bea766c56d569a"> 17869 <button id="chat-toggle-btn" class="chat-toggle-btn" aria-label="Toggle chat"> 17870 <svg class="chat-icon" width="28" height="28" viewBox="0 0 640 640" fill="currentColor"> 17871 <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 64
17871C443.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"/> 17872 </svg> 17873 <span id="chat-unread-badge" class="chat-unread-badge" style="display: none;">0</span> 17874 </button> 17875 17876 <div id="chat-popup" class="chat-popup" style="display: none;"> 17877 <div class="chat-popup-header"> 17878 <div class="chat-header-title"> 17879 <h3>Bantuan</h3> 17880 <span id="chatOperatingHours" class="chat-operating-hours" style="display: none;"></span> 17881 </div> 17882 <div class="chat-header-actions"> 17883 <button id="chatNotifToggle" class="btn-notif-toggle" data-status="not-supported" title="Push notification" style="display: none;"> 17884 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 17885 <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"/> 17886 </svg> 17887 </button> 17888 <button id="chat-close-btn" class="chat-close-btn" aria-label="Close chat">Ã</button> 17889 </div> 17890 </div> 17891 17892 <!-- Notification Info Bar (conditional) --> 17893 <div class="chat-notif-info-bar" id="chatNotifInfoBar" style="display:none;"> 17894 <svg width="14" height="14" viewBox="0 0 24 24" fill="#EAB308"> 17895 <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"/> 17896 </svg> 17897 <span class="small">Aktifkan notifikasi untuk pemberitahuan balasan</span> 17898 <button class="btn-dismiss" onclick="ChatNotification.dismissInfo()">â</button> 17899 </div> 17900 17901 <div class="chat-popup-body"> 17902 <!-- Conversations List View --> 17903 <div id="chat-list-view" class="chat-view"> 17904 <div id="chat-conversations-list" class="chat-conversations-list"> 17905 <div class="chat-loading">Memuat...</div> 17906 <!-- Conversation Item Structure (rendered by JS): 17907 <div class="chat-conversation-item"> 17908 <div class="conversation-avatar"> 17909 <svg>...</svg> 17910 </div> 17911 <div class="conversation-content"> 17912 <div class="conversation-header"> 17913 <span class="conversation-name">Admin Support</span> 17914 <span class="conversation-time">10:30</span> 17915 </div> 17916 <div class="conversation-preview"> 17917 <p class="conversation-message">Terima kasih telah menghubungi kami...</p> 17918 <span class="conversation-unread">2</span> 17919 </div> 17920 </div> 17921 </div> 17922 --> 17923 </div> 17924 <button id="chat-new-btn" class="chat-new-btn">
17925 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17926 <line x1="12" y1="5" x2="12" y2="19"></line> 17927 <line x1="5" y1="12" x2="19" y2="12"></line> 17928 </svg> 17929 Percakapan Baru 17930 </button> 17931 </div> 17932 17933 <!-- Chat Messages View --> 17934 <div id="chat-messages-view" class="chat-view" style="display: none;"> 17935 <div id="chat-messages-container" class="chat-messages-container"> 17936 <div class="chat-loading">Memuat pesan...</div> 17937 <!-- Message Structure (rendered by JS): 17938 <div class="chat-message"> (or .chat-message.mine) 17939 <div class="chat-message-bubble"> 17940 <div class="message-sender">Admin Support</div> (only for non-mine) 17941 <div class="message-text">Halo, ada yang bisa dibantu?</div> 17942 <div class="message-meta"> 17943 <span class="message-time">10:30</span> 17944 <svg class="message-status">...</svg> (only for mine) 17945 </div> 17946 </div> 17947 </div> 17948 --> 17949 </div> 17950 17951 <form id="chat-message-form" class="chat-message-form"> 17952 <input type="hidden" id="chat-current-conversation-id" value=""> 17953 <textarea 17954 id="chat-message-input" 17955 class="chat-message-input" 17956 placeholder="Ketik pesan..." 17957 rows="1" 17958 maxlength="5000" 17959 ></textarea> 17960 <button type="submit" class="chat-send-btn" aria-label="Send message"> 17961 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17962 <line x1="22" y1="2" x2="11" y2="13"></line> 17963 <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 17964 </svg> 17965 </button> 17966 </form> 17967 </div> 17968 17969 <!-- New Chat View --> 17970 <div id="chat-new-view" class="chat-view" style="display: none;"> 17971 <div class="chat-messages-header"> 17972 <button id="chat-new-back-btn" class="chat-back-btn" aria-label="Back to list"> 17973 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17974 <path d="M19 12H5M12 19l-7-7 7-7"/> 17975 </svg> 17976 </button> 17977 <div class="chat-conversation-title">Percakapan Baru</div> 17978 </div> 17979 17980 <div class="chat-new-form"> 17981 <form id="chat-start-form"> 17982 <div class="form-group"> 17983 <label for="chat-new-subject">Subjek</label> 17984 <input 17985 type="text" 17986 id="chat-new-subject" 17987 class="form-control" 17988 placeholder="Bantuan Pelanggan" 17989 maxlength="255" 17990 > 17991 </div> 17992 <div class="form-group"> 17993 <label for="chat-new-message">Pesan</label> 17994 <textarea 17995 id="chat-new-message" 17996 class="form-control" 17997 rows="4" 17998 placeholder="Tulis pertanyaan Anda..." 17999 required 18000 maxlength="5000" 18001 ></textarea> 18002 </div> 18003 <button type="submit" class="btn btn-primary btn-block">Kirim</button> 18004 </form> 18005 </div> 18006 </div> 18007 </div> 18008 </div> 18009 </div> 18010 18011 <!-- Login Required Modal --> 18012 <div class="modal-overlay" id="chatLoginModal"> 18013 <div class="modal-dialog"> 18014 <div class="modal-header"> 18015 <h3 class="modal-title"> 18016 <div class="modal-icon info"> 18017 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18018 <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"/> 18019 </svg> 18020 </div> 18021 Login Diperlukan 18022 </h3> 18023 </div> 18024 <div class="modal-body"> 18025 <p class="modal-message">
18025Anda harus login terlebih dahulu untuk menggunakan fitur chat bantuan pelanggan.</p> 18026 <div class="modal-actions"> 18027 <button class="modal-btn modal-btn-secondary" id="chatLoginCancel">Batal</button> 18028 <button class="modal-btn modal-btn-primary" id="chatLoginConfirm">Login Sekarang</button> 18029 </div> 18030 </div> 18031 </div> 18032 </div> 18033
18033<script src="https://plazait.co.id/assets/js/chat-widget.js" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script>
18033<script data-inlined="chat-notification.js"> 18034/** 18035 * Customer Chat Push Notification Manager 18036 * Handles browser push notification subscription for customers 18037 * Version: 1.0.0 18038 * Date: 2026-01-19 18039 */ 18040 18041const ChatNotification = { 18042 // State 18043 status: 'not-supported', // not-supported, not-subscribed, subscribed, blocked 18044 subscription: null, 18045 vapidPublicKey: null, // Will be loaded from server 18046 18047 /** 18048 * Initialize notification manager 18049 */ 18050 async init() { 18051 // Check if user is logged in (only for logged-in users) 18052 if (!this.isUserLoggedIn()) { 18053 this.updateUI('not-supported'); 18054 return; 18055 } 18056 18057 // Check browser support 18058 if (!this.isSupported()) { 18059 this.updateUI('not-supported'); 18060 return; 18061 } 18062 18063 // Load VAPID key from server 18064 await this.loadVapidKey(); 18065 18066 if (!this.vapidPublicKey) { 18067 this.updateUI('not-supported'); 18068 return; 18069 } 18070 18071 // Register service worker if needed 18072 await this.registerServiceWorker(); 18073 18074 // Check current permission 18075 const permission = Notification.permission; 18076 18077 if (permission === 'denied') { 18078 this.updateUI('blocked'); 18079 } else if (permission === 'granted') { 18080 // Check if already subscribed 18081 const isSubscribed = await this.checkSubscriptionStatus(); 18082 this.updateUI(isSubscribed ? 'subscribed' : 'not-subscribed'); 18083 } else { 18084 // Default: not subscribed yet 18085 this.updateUI('not-subscribed'); 18086 this.showInfoBar(); 18087 } 18088 }, 18089 18090 /** 18091 * Check if user is logged in 18092 */ 18093 isUserLoggedIn() { 18094 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 18095 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 18096 }, 18097 18098 /** 18099 * Check if browser supports push notifications 18100 */ 18101 isSupported() { 18102 return ( 18103 'serviceWorker' in navigator && 18104 'PushManager' in window && 18105 'Notification' in window 18106 ); 18107 }, 18108 18109 /** 18110 * Load VAPID public key from server 18111 */ 18112 async loadVapidKey() { 18113 try { 18114 const response = await fetch('/api/chat/notifications/vapid_key', { 18115 method: 'GET', 18116 headers: { 18117 'Accept': 'application/json' 18118 } 18119 }); 18120 18121 if (!response.ok) { 18122 return false; 18123 } 18124 18125 const data = await response.json(); 18126 if (data.success && data.data?.public_key) { 18127 this.vapidPublicKey = data.data.public_key; 18128 return true; 18129 } 18130 18131 return false; 18132 18133 } catch (error) { 18134 console.error('[ChatNotification] Error loading VAPID key:', error); 18135 return false; 18136 } 18137 }, 18138 18139 /** 18140 * Register service worker 18141 */ 18142 async registerServiceWorker() { 18143 if (!('serviceWorker' in navigator)) return null; 18144 18145 try { 18146 const registration = await navigator.serviceWorker.register('/sw-push.js', { 18147 scope: '/' 18148 }); 18149 return registration; 18150 } catch (error) { 18151 return null; 18152 } 18153 }, 18154 18155 /** 18156 * Check subscription status from server 18157 */ 18158 async checkSubscriptionStatus() { 18159 try { 18160 const response = await fetch('/api/chat/notifications/status', { 18161 method: 'GET', 18162 credentials: 'include', 18163 headers: { 18164 'Accept': 'application/json' 18165 } 18166 }); 18167 18168 if (!response.ok) { 18169 return false; 18170 } 18171 18172 const data = await response.json(); 18173 return data.data?.subscribed || false; 18174 18175 } catch (error) { 18176 console.error('[ChatNotification] Error checking subscription:', error); 18177 return false; 18178 } 18179 }, 18180 18181 /** 18182 * Update UI based on status 18183 */ 18184 updateUI(status) { 18185 this.status = status; 18186 const btn = document.getElementById('chatNotifToggle'); 18187 18188 if (!btn) { 18189 // Retry after a short delay (button might not be in DOM yet) 18190 setTimeout(() => this.updateUI(status), 100); 18191 return; 18192 } 18193 18194 btn.setAttribute('data-status', status); 18195 18196 // Show/hide button based on support 18197 if (status === 'not-supported') { 18198 btn.style.display = 'none'; 18199 } else { 18200 btn.style.display = 'block'; 18201 } 18202 18203 // Update SVG icon (bell vs bell-slash) 18204 const svg = btn.querySelector('svg'); 18205 if (svg) { 18206 if (status === 'subscribed') { 18207 // Bell icon (active) 18208 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"/>'; 18209 } else { 18210 // Bell slash icon (inactive/blocked) 18211 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"/>'; 18212 } 18213 } 18214 18215 // Update tooltip 18216 const tooltips = { 18217 'not-subscribed': 'Klik untuk aktifkan notifikasi', 18218 'subscribed': 'Notifikasi aktif', 18219 'blocked': 'Notifikasi diblokir'
18220 }; 18221 18222 if (tooltips[status]) { 18223 btn.title = tooltips[status]; 18224 } 18225 18226 // Hide info bar if subscribed or blocked 18227 if (status === 'subscribed' || status === 'blocked') { 18228 this.hideInfoBar(); 18229 } 18230 }, 18231 18232 /** 18233 * Show info bar (if not dismissed) 18234 */ 18235 showInfoBar() { 18236 if (localStorage.getItem('chat_notif_info_dismissed')) { 18237 return; 18238 } 18239 18240 const infoBar = document.getElementById('chatNotifInfoBar'); 18241 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18242 18243 if (infoBar) { 18244 infoBar.style.display = 'flex'; 18245 18246 // Bind dismiss button here so it's always attached when bar is visible 18247 const btn = infoBar.querySelector('#chatNotifDismissBtn'); 18248 if (btn && !btn._dismissBound) { 18249 btn._dismissBound = true; 18250 btn.addEventListener('click', () => { 18251 infoBar.style.display = 'none'; 18252 localStorage.setItem('chat_notif_info_dismissed', '1'); 18253 if (popupBody) popupBody.classList.remove('has-info-bar'); 18254 }); 18255 } 18256 } 18257 18258 if (popupBody) { 18259 popupBody.classList.add('has-info-bar'); 18260 } 18261 }, 18262 18263 /** 18264 * Hide info bar 18265 */ 18266 hideInfoBar() { 18267 const infoBar = document.getElementById('chatNotifInfoBar'); 18268 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18269 18270 if (infoBar) { 18271 infoBar.style.display = 'none'; 18272 } 18273 18274 if (popupBody) { 18275 popupBody.classList.remove('has-info-bar'); 18276 } 18277 }, 18278 18279 /** 18280 * Dismiss info bar permanently 18281 */ 18282 dismissInfo() { 18283 this.hideInfoBar(); 18284 localStorage.setItem('chat_notif_info_dismissed', '1'); 18285 }, 18286 18287 /** 18288 * Handle bell icon click 18289 */ 18290 async handleToggleClick() { 18291 if (this.status === 'not-subscribed') { 18292 await this.subscribe(); 18293 } else if (this.status === 'subscribed') { 18294 await this.unsubscribe(); 18295 } else if (this.status === 'blocked') { 18296 this.showUnblockHelp(); 18297 } 18298 }, 18299 18300 /** 18301 * Subscribe to push notifications 18302 */ 18303 async subscribe() { 18304 try { 18305 // Request permission 18306 const permission = await Notification.requestPermission(); 18307 18308 if (permission === 'denied') { 18309 this.updateUI('blocked'); 18310 this.showToast('Notifikasi diblokir', 'error'); 18311 return; 18312 } 18313 18314 if (permission !== 'granted') { 18315 return; 18316 } 18317 18318 // Get service worker registration 18319 const registration = await navigator.serviceWorker.ready; 18320 18321 // Subscribe to push 18322 const subscription = await registration.pushManager.subscribe({ 18323 userVisibleOnly: true, 18324 applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) 18325 }); 18326 18327 // Send subscription to server 18328 const response = await fetch('/api/chat/notifications/subscribe', { 18329 method: 'POST', 18330 credentials: 'include', 18331 headers: { 18332 'Content-Type': 'application/json', 18333 'Accept': 'application/json' 18334 }, 18335 body: JSON.stringify(subscription.toJSON()) 18336 }); 18337 18338 if (!response.ok) { 18339 throw new Error('Failed to save subscription'); 18340 } 18341 18342 const result = await response.json(); 18343 18344 this.subscription = subscription; 18345 this.updateUI('subscribed'); 18346 this.hideInfoBar(); 18347 this.showToast('Notifikasi berhasil diaktifkan!', 'success'); 18348 18349 // Send test notification 18350 setTimeout(() => this.sendTestNotification(), 1000); 18351 18352 } catch (error) { 18353 console.error('[ChatNotification] Subscribe error:', error); 18354 this.showToast('Gagal mengaktifkan notifikasi', 'error'); 18355 } 18356 }, 18357 18358 /** 18359 * Unsubscribe from push notifications 18360 */ 18361 async unsubscribe() { 18362 try { 18363 // Confirm first 18364 if (!confirm('Nonaktifkan notifikasi chat?')) { 18365 return; 18366 } 18367 18368 // Get current subscription 18369 const registration = await navigator.serviceWorker.ready; 18370 const subscription = await registration.pushManager.getSubscription(); 18371 18372 if (subscription) { 18373 // Unsubscribe from browser 18374 await subscription.unsubscribe(); 18375 18376 // Tell server 18377 await fetch('/api/chat/notifications/unsubscribe', { 18378 method: 'POST', 18379 credentials: 'include', 18380 headers: { 18381 'Content-Type': 'application/json', 18382 'Accept': 'application/json' 18383 }, 18384 body: JSON.stringify(subscription.toJSON()) 18385 }); 18386 } 18387 18388 this.subscription = null; 18389 this.updateUI('not-subscribed'); 18390 this.showInfoBar(); 18391 this.showToast('Notifikasi dinonaktifkan', 'info'); 18392 18393 } catch (error) { 18394 console.error('[ChatNotification] Unsubscribe error:', error); 18395 this.showToast('Gagal menonaktifkan notifikasi', 'error'); 18396 } 18397 }, 18398 18399 /** 18400 * Send test notification 18401 */ 18402 async sendTestNotification() { 18403 try { 18404 const response = await fetch('/api/chat/notifications/send_test', { 18405 method: 'POST', 18406 credentials: 'include', 18407 headers: { 18408 'Accept': 'application/json' 18409 } 18410 }); 18411 18412 if (response.ok) { 18413 // Test notification sent successfully 18414 } 18415 } catch (error) { 18416 // Test notification failed silently 18417 } 18418 }, 18419 18420 /** 18421 * Show unblock help modal/message 18422 */ 18423 showUnblockHelp() { 18424 const message = ` 18425 Notifikasi diblokir oleh browser.\n\n 18426 Untuk mengaktifkan:\n 18427 1. Klik ikon gembok/info di address bar\n 18428 2. Cari "Notifications" atau "Notifikasi"\n 18429 3. Ubah menjadi "Allow" atau "Izinkan"\n 18430 4. Refresh halaman ini 18431 `.trim().replace(/\s+/g, ' '); 18432 18433 alert(message); 18434 }, 18435 18436 /** 18437 * Show toast notification 18438 */ 18439 showToast(message, type = 'info') { 18440 // Simple toast implementation 18441 const toast = document.createElement('div'); 18442 toast.className = `chat-toast chat-toast-${type}`; 18443 toast.textContent = message; 18444 toast.style.cssText = ` 18445 position: fixed; 18446 bottom: 80px; 18447 right: 20px; 18448 background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; 18449 color: white; 18450 padding: 12px 20px; 18451 border-radius: 4px; 18452 box-shadow: 0 2px 8px rgba(0,0,0,0.2); 18453 z-index: 10000; 18454 animation: slideInUp 0.3s ease; 18455 `; 18456 18457 document.body.appendChild(toast); 18458 18459 setTimeout(() => { 18460 toast.style.animation = 'slideOutDown 0.3s ease'; 18461 setTimeout(() => toast.remove(), 300); 18462 }, 3000); 18463 }, 18464 18465 /** 18466 * Convert VAPID key to Uint8Array 18467 */ 18468 urlBase64ToUint8Array(base64String) { 18469 const padding = '='.repeat((4 - base64String.length % 4) % 4); 18470 const base64 = (base64String + padding) 18471 .replace(/\-/g, '+') 18472 .replace(/_/g, '/'); 18473 18474 const rawData = window.atob(base64); 18475 const outputArray = new Uint8Array(rawData.length); 18476 18477 for (let i = 0; i < rawData.length; ++i) { 18478 outputArray[i] = rawData.charCodeAt(i); 18479 } 18480 return outputArray; 18481 }, 18482 18483 /** 18484 * Check if push notification is currently active 18485 * @returns {Promise<boolean>} 18486 */ 18487 async isPushActive() { 18488 try { 18489 // Check browser subscription 18490 if (!('serviceWorker' in navigator)) return false; 18491 18492 const registration = await navigator.serviceWorker.getRegistration(); 18493 if (!registration) return false; 18494 18495 const subscription = await registration.pushManager.getSubscription(); 18496 if (!subscription) return false; 18497 18498 // Also check server status 18499 const serverStatus = await this.checkSubscriptionStatus();
18500 18501 return serverStatus; 18502 } catch (error) { 18503 return false; 18504 } 18505 }, 18506 18507 /** 18508 * Show logout confirmation if push notification is active 18509 * Call this before logout action 18510 * @returns {Promise<boolean>} true if user confirms logout, false if cancelled 18511 */ 18512 async confirmLogoutWithPushNotification() { 18513 const isActive = await this.isPushActive(); 18514 18515 if (!isActive) { 18516 // No active push, proceed with logout 18517 return true; 18518 } 18519 18520 // Show modal confirmation using modal.css 18521 return new Promise((resolve) => { 18522 // Create modal HTML 18523 const modalHTML = ` 18524 <div class="modal-overlay" id="logoutConfirmModal"> 18525 <div class="modal-dialog"> 18526 <div class="modal-header"> 18527 <h3 class="modal-title"> 18528 <span class="modal-icon warning">â ï¸</span> 18529 Konfirmasi Logout 18530 </h3> 18531 </div> 18532 <div class="modal-body"> 18533 <p class="modal-message"> 18534 Anda memiliki <strong>notifikasi push yang aktif</strong>.<br><br> 18535 Setelah logout, Anda tidak akan menerima notifikasi chat lagi di perangkat ini.<br><br> 18536 Apakah Anda yakin ingin melanjutkan logout? 18537 </p> 18538 </div> 18539 <div class="modal-actions" style="padding: 0 20px 20px;"> 18540 <button class="modal-btn modal-btn-secondary" id="logoutCancelBtn"> 18541 Batal 18542 </button> 18543 <button class="modal-btn modal-btn-danger" id="logoutConfirmBtn"> 18544 Lanjutkan Logout 18545 </button> 18546 </div> 18547 </div> 18548 </div> 18549 `; 18550 18551 // Append to body 18552 const modalContainer = document.createElement('div'); 18553 modalContainer.innerHTML = modalHTML; 18554 document.body.appendChild(modalContainer.firstElementChild); 18555 18556 const modal = document.getElementById('logoutConfirmModal'); 18557 const confirmBtn = document.getElementById('logoutConfirmBtn'); 18558 const cancelBtn = document.getElementById('logoutCancelBtn'); 18559 18560 // Show modal with animation 18561 requestAnimationFrame(() => { 18562 modal.classList.add('show'); 18563 }); 18564 18565 // Handle confirm 18566 const handleConfirm = () => { 18567 cleanup(); 18568 resolve(true); 18569 }; 18570 18571 // Handle cancel 18572 const handleCancel = () => { 18573 cleanup(); 18574 resolve(false); 18575 }; 18576 18577 // Cleanup function 18578 const cleanup = () => { 18579 modal.classList.remove('show'); 18580 setTimeout(() => { 18581 modal.remove(); 18582 }, 200); 18583 confirmBtn.removeEventListener('click', handleConfirm); 18584 cancelBtn.removeEventListener('click', handleCancel); 18585 modal.removeEventListener('click', handleBackdropClick); 18586 }; 18587 18588 // Handle backdrop click 18589 const handleBackdropClick = (e) => { 18590 if (e.target === modal) { 18591 handleCancel(); 18592 } 18593 }; 18594 18595 // Add event listeners 18596 confirmBtn.addEventListener('click', handleConfirm); 18597 cancelBtn.addEventListener('click', handleCancel); 18598 modal.addEventListener('click', handleBackdropClick); 18599 }); 18600 } 18601}; 18602 18603// Add CSS animations 18604const chatNotifStyle = document.createElement('style'); 18605chatNotifStyle.textContent = ` 18606 @keyframes slideInUp { 18607 from { 18608 transform: translateY(100%); 18609 opacity: 0; 18610 } 18611 to { 18612 transform: translateY(0); 18613 opacity: 1;
18614 } 18615 } 18616 18617 @keyframes slideOutDown { 18618 from { 18619 transform: translateY(0); 18620 opacity: 1; 18621 } 18622 to { 18623 transform: translateY(100%); 18624 opacity: 0; 18625 } 18626 } 18627`; 18628document.head.appendChild(chatNotifStyle); 18629 18630// Auto-initialize when DOM is ready 18631if (document.readyState === 'loading') { 18632 document.addEventListener('DOMContentLoaded', () => ChatNotification.init()); 18633} else { 18634 ChatNotification.init(); 18635} 18636 18637// Expose to window for access from other scripts 18638window.ChatNotification = ChatNotification; 18639 18640</script>
18640 18641 <!-- Footer Collapse Toggle Script --> 18642
18642<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 18643 document.addEventListener('DOMContentLoaded', function() { 18644 const ftTop = document.getElementById('ftTop'); 18645 const expandBtn = document.getElementById('ftExpandBtn'); 18646 18647 if (ftTop && expandBtn) { 18648 const expandText = expandBtn.querySelector('.expand-text'); 18649 18650 expandBtn.addEventListener('click', function() { 18651 ftTop.classList.toggle('expanded'); 18652 const isExpanded = ftTop.classList.contains('expanded'); 18653 if (expandText) { 18654 expandText.textContent = isExpanded ? 'Sembunyikan' : 'Selengkapnya'; 18655 } 18656 }); 18657 } 18658 }); 18659 </script>
18659 18660 </footer> 18661 18662<!-- Compare Sidebar Overlay --> 18663<div class="compare-overlay" id="compareOverlay"></div> 18664 18665<!-- Floating Compare Badge (Sticky) --> 18666<button class="compare-floating-badge" id="compareFloatingBadge" aria-label="Lihat produk yang dibandingkan" style="display:none;"> 18667 <svg class="cfb-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 18668 <path d="M9.01 14H2v2h7.01v3L13 15l-3.99-4v3zm5.98-1v-3H22V8h-7.01V5L11 9l3.99 4z"/> 18669 </svg> 18670 <span class="cfb-text">Bandingkan</span> 18671 <span class="cfb-count" id="cfbCount">0</span> 18672</button> 18673 18674<!-- AI Comparison Modal --> 18675<div class="ai-compare-modal" id="aiCompareModal"> 18676 <div class="modal-backdrop"></div> 18677 <div class="modal-container"> 18678 <div class="modal-header"> 18679 <h3 class="modal-title"> 18680 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 18681 <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"/> 18682 </svg> 18683 Analisis AI: Perbandingan Produk 18684 </h3> 18685 <button class="modal-close" id="aiCompareModalClose" aria-label="Tutup"> 18686 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18687 <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"/> 18688 </svg> 18689 </button> 18690 </div> 18691 18692 <div class="modal-content"> 18693 <!-- Loading State --> 18694 <div class="ai-loading" id="aiLoading"> 18695 <div class="loading-animation"> 18696 <div class="typing-indicator"> 18697 <span></span> 18698 <span></span> 18699 <span></span> 18700 </div> 18701 <p>AI sedang menganalisis produk...</p> 18702 <small>Proses ini membutuhkan waktu 10-30 detik</small> 18703 </div> 18704 </div> 18705 18706 <!-- Error State --> 18707 <div class="ai-error" id="aiError" hidden> 18708 <div class="error-icon"> 18709 <svg width="48" height="48" viewBox="0 0 24 24" fill="#ef4444"> 18710 <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"/> 18711 </svg> 18712 </div> 18713 <h4>Analisis AI Gagal</h4> 18714 <p id="aiErrorMessage">Terjadi kesalahan saat memproses analisis.</p> 18715 <button class="btn btn-outline-primary" id="aiRetry">Coba Lagi</button> 18716 </div> 18717 18718 <!-- Analysis Results --> 18719 <div class="ai-results" id="aiResults" hidden> 18720 <!-- Summary Section --> 18721 <div class="ai-summary-card"> 18722 <h4> 18723 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18724 <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"/> 18725 </svg> 18726 Ringkasan Analisis 18727 </h4> 18728 <p id="aiSummary" class="summary-text"></p> 18729 <div class="winner-badge" id="winnerBadge"> 18730 <strong>Rekomendasi Terbaik:</strong> <span id="winnerProduct"></span> 18731 <small id="winnerReason"></small> 18732 </div> 18733 </div> 18734 18735 <!-- Comparison Table --> 18736 <div class="ai-comparison-table"> 18737 <h4> 18738 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18739 <path d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"/> 18740 </svg> 18741 Perbandingan Detail 18742 </h4> 18743 <div id="comparisonTable" class="comparison-grid"></div> 18744 </div> 18745 18746 <!-- Detailed Analysis --> 18747 <div class="ai-detailed-analysis"> 18748 <h4> 18749 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18750 <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"/> 18751 </svg> 18752 Analisis Mendalam 18753 </h4> 18754 <div id="detailedAnalysis" class="products-analysis"></div> 18755 </div> 18756 18757 <!-- Recommendations --> 18758 <div class="ai-recommendations"> 18759 <h4> 18760 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18761 <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"/> 18762 </svg> 18763 Rekomendasi Berdasarkan Kebutuhan 18764 </h4> 18765 <div class="recommendations-grid"> 18766 <div class="recommendation-card"> 18767 <h5>ð° Budget Conscious</h5> 18768 <p id="budgetRecommendation"></p> 18769 </div> 18770 <div class="recommendation-card"> 18771 <h5>â¡ Performance Focused</h5> 18772 <p id="performanceRecommendation"></p> 18773 </div> 18774 <div class="recommendation-card"> 18775 <h5>âï¸ Balanced Choice</h5> 18776 <p id="balancedRecommendation"></p> 18777 </div> 18778 </div> 18779 </div> 18780 18781 <!-- Buying Tips --> 18782 <div class="ai-tips"> 18783 <h4> 18784 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18785 <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
18785M5,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"/> 18786 </svg> 18787 Tips Pembelian 18788 </h4> 18789 <ul id="buyingTips" class="tips-list"></ul> 18790 </div> 18791 </div> 18792 </div> 18793 </div> 18794</div> 18795 18796<!-- Cart Login Modal --> 18797<div class="modal-overlay" id="cartLoginModal"> 18798 <div class="modal-dialog"> 18799 <div class="modal-header"> 18800 <h3 class="modal-title"> 18801 <div class="modal-icon info"> 18802 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18803 <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"/> 18804 </svg> 18805 </div> 18806 Login untuk Melanjutkan 18807 </h3> 18808 </div> 18809 <div class="modal-body"> 18810 <p class="modal-message" style="margin-bottom: 16px;">Silakan login atau daftar untuk mengakses keranjang belanja dan menyelesaikan pesanan Anda.</p> 18811 <div style="background: #f8fafc; border-radius: 8px; padding: 12px; margin-bottom: 20px;"> 18812 <p style="font-size: 14px; color: #646947; margin: 0; line-height: 1.6;"> 18813 <strong style="color: #2a2a0f;">Keuntungan dengan akun:</strong><br> 18814 â Simpan keranjang belanja Anda<br> 18815 â Lacak status pesanan secara real-time<br> 18816 â Checkout lebih cepat dengan data tersimpan<br> 18817 â Dapatkan notifikasi promo & diskon eksklusif 18818 </p> 18819 </div> 18820 <div class="modal-actions"> 18821 <button class="modal-btn modal-btn-secondary" id="cartLoginCancel">Batal</button> 18822 <button class="modal-btn modal-btn-primary" id="cartLoginConfirm" style="background: #ff6b35; border-color: #ff6b35;">Login</button> 18823 <button class="modal-btn modal-btn-primary" id="cartRegisterBtn" style="background: #f7931e; border-color: #f7931e;">Daftar</button> 18824 </div> 18825 </div> 18826 </div> 18827</div> 18828 18829
18830<script data-inlined="app.js"> 18831(function initLiveSearch(){ 18832 function setup() { 18833 const searchInput = document.getElementById('searchInput'); 18834 const suggest = document.getElementById('searchSuggest'); 18835 const searchBtn = document.getElementById('searchBtn'); 18836 const aiToggleBtn = document.getElementById('aiToggleBtn'); 18837 18838 if (!searchInput || !suggest || !aiToggleBtn) { 18839 console.log('[LiveSearch] Elemen belum ada, retry...'); 18840 return false; 18841 } 18842 18843 let debounceTimer = null; 18844 let lastQuery = ''; 18845 let currentIndex = -1; 18846 let currentAbortController = null; 18847 let isAIModeEnabled = false; // AI mode OFF by default 18848 let isAIProcessing = false; // Flag to prevent closing suggest box during AI processing 18849 18850 // Trending cache 18851 let trendingCache = null; 18852 let trendingLoading = false; 18853 18854 // Trending products cache 18855 let trendingProductsCache = null; 18856 let trendingProductsLoading = false; 18857 18858 const MIN_LEN = 2; 18859 const DEBOUNCE = 300; 18860 const AI_TIMEOUT = 15000; // 15 seconds timeout for AI endpoint 18861 18862 // Load AI mode preference from localStorage 18863 const savedAIMode = localStorage.getItem('plazait_ai_search_mode'); 18864 if (savedAIMode === 'enabled') { 18865 isAIModeEnabled = false; 18866 aiToggleBtn.classList.remove('active'); 18867 } 18868 18869 // Update placeholder based on AI mode 18870 function updatePlaceholder() { 18871 if (isAIModeEnabled) { 18872 searchInput.placeholder = 'ð¤ Coba: "laptop HP core i3 untuk kerja" atau "tinta epson l3210"'; 18873 } else { 18874 // Static placeholder for typing animation 18875 searchInput.placeholder = ''; 18876 } 18877 } 18878 18879 // Typing animation for placeholder 18880 let typingTimeout = null; 18881 let currentProductIndex = 0; 18882 let currentCharIndex = 0; 18883 let isTyping = false; 18884 let isDeleting = false; 18885 18886 const popularProducts = [ 18887 'Oppo A6', 18888 'Asus Ryzen 3', 18889 'Starlink V4', 18890 'HP Victus', 18891 'Lenovo RTX 5060', 18892 'ROG Strix', 18893 'iPhone 13', 18894 'Xpad 20', 18895 'Xiaomi 14', 18896 'Epson L3210', 18897 'Realme C85', 18898 'Brother T720', 18899 'Mouse Logitech' 18900 ]; 18901 18902 function typeWriter() { 18903 if (isAIModeEnabled || document.activeElement === searchInput) { 18904 return; // Skip typing if AI mode is on or input is focused 18905 } 18906 18907 const currentProduct = popularProducts[currentProductIndex]; 18908 18909 if (!isDeleting) { 18910 // Typing forward 18911 if (currentCharIndex < currentProduct.length) { 18912 searchInput.placeholder = currentProduct.substring(0, currentCharIndex + 1); 18913 currentCharIndex++; 18914 typingTimeout = setTimeout(typeWriter, 100); // Typing speed 18915 } else { 18916 // Finished typing, wait before deleting 18917 typingTimeout = setTimeout(() => { 18918 isDeleting = true; 18919 typeWriter(); 18920 }, 2000); // Pause before deleting 18921 } 18922 } else { 18923 // Deleting backward 18924 if (currentCharIndex > 0) { 18925 searchInput.placeholder = currentProduct.substring(0, currentCharIndex - 1); 18926 currentCharIndex--; 18927 typingTimeout = setTimeout(typeWriter, 50); // Deleting speed (faster) 18928 } else { 18929 // Finished deleting, move to next product 18930 isDeleting = false; 18931 currentProductIndex = (currentProductIndex + 1) % popularProducts.length; 18932 typingTimeout = setTimeout(typeWriter, 500); // Pause before typing next 18933 } 18934 } 18935 } 18936 18937 function startTypingAnimation() { 18938 if (!isAIModeEnabled && !typingTimeout) { 18939 currentCharIndex = 0; 18940 isDeleting = false; 18941 typeWriter(); 18942 } 18943 } 18944 18945 function stopTypingAnimation() { 18946 if (typingTimeout) { 18947 clearTimeout(typingTimeout); 18948 typingTimeout = null; 18949 } 18950 } 18951 18952 // Set initial placeholder 18953 updatePlaceholder(); 18954 18955 // Start typing animation for normal mode 18956 if (!isAIModeEnabled) { 18957 startTypingAnimation(); 18958 } 18959 18960 function esc(str){ 18961 if (str === null || str === undefined) return ''; 18962 return String(str) 18963 .replace(/&/g,'&') 18964 .replace(/</g,'<') 18965 .replace(/>/g,'>') 18966 .replace(/"/g,'"') 18967 .replace(/'/g,'''); 18968 } 18969 18970 function clearSuggest(){ 18971 suggest.innerHTML = ''; 18972 suggest.hidden = true; 18973 suggest.classList.remove('open', 'trending-mode'); 18974 currentIndex = -1; 18975 } 18976 18977 // ==================== TRENDING FUNCTIONS ==================== 18978 18979 async function fetchTrending() { 18980 if (trendingCache) return trendingCache; 18981 if (trendingLoading) return null; 18982 18983 trendingLoading = true; 18984
18985 try { 18986 const res = await fetch('/api/trending_searches.php?timeframe=7d&limit=8'); 18987 const data = await res.json(); 18988 18989 if (data.ok && Array.isArray(data.data)) { 18990 trendingCache = data.data; 18991 console.log('[Trending] Loaded', trendingCache.length, 'items'); 18992 return trendingCache; 18993 } 18994 } catch (err) { 18995 console.error('[Trending] Fetch error:', err); 18996 } finally { 18997 trendingLoading = false; 18998 } 18999 19000 return null; 19001 } 19002 19003 async function fetchTrendingProducts() { 19004 if (trendingProductsCache) return trendingProductsCache; 19005 if (trendingProductsLoading) return null; 19006 19007 trendingProductsLoading = true; 19008 19009 try { 19010 const res = await fetch('/api/trending_products.php?timeframe=7d&limit=6'); 19011 const data = await res.json(); 19012 19013 if (data.ok && Array.isArray(data.data)) { 19014 trendingProductsCache = data.data; 19015 console.log('[Trending Products] Loaded', trendingProductsCache.length, 'items'); 19016 return trendingProductsCache; 19017 } 19018 } catch (err) { 19019 console.error('[Trending Products] Fetch error:', err); 19020 } finally { 19021 trendingProductsLoading = false; 19022 } 19023 19024 return null; 19025 } 19026 19027 function renderTrendingSuggestions(trends, products) { 19028 if ((!trends || trends.length === 0) && (!products || products.length === 0)) { 19029 clearSuggest(); 19030 return; 19031 } 19032 19033 let html = `<div class="trending-container">`; 19034 19035 // Left column: Trending searches 19036 if (trends && trends.length > 0) { 19037 html += ` 19038 <div class="trending-column trending-searches"> 19039 <div class="trending-header"> 19040 <span class="trending-icon">ð¥</span> 19041 <span class="trending-title">Pencarian Trending</span> 19042 </div> 19043 `; 19044 19045 html += trends.map((t, idx) => { 19046 const categoryHint = t.category_hint ? `<span class="trending-category">${esc(t.category_hint)}</span>` : ''; 19047 return ` 19048 <div class="trending-item" data-query="${esc(t.query)}" data-index="${idx}"> 19049 <span class="trending-rank"> 19050 <img src="/assets//img/uptrend.image" alt="trending" width="16" height="16" style="vertical-align: middle;"> 19051 </span> 19052 <span class="trending-query">${esc(t.query)}</span> 19053 ${categoryHint} 19054 </div> 19055 `; 19056 }).join(''); 19057 19058 html += `</div>`; 19059 } 19060 19061 // Right column: Trending products 19062 if (products && products.length > 0) { 19063 html += ` 19064 <div class="trending-column trending-products"> 19065 <div class="trending-header"> 19066 <span class="trending-icon">â</span> 19067 <span class="trending-title">Produk Populer</span> 19068 </div> 19069 `; 19070 19071 html += products.map((p, idx) => { 19072 return ` 19073 <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}"> 19074 <img src="${esc(p.image)}" alt="${esc(p.name)}" class="trending-product-thumb"> 19075 <div class="trending-product-info"> 19076 <div class="trending-product-name">${esc(p.name)}</div> 19077 <div class="trending-product-price">${esc(p.price_label)}</div> 19078 </div> 19079 </div> 19080 `; 19081 }).join(''); 19082 19083 html += `</div>`; 19084 } 19085 19086 html += `</div>`; 19087 19088 suggest.innerHTML = html; 19089 suggest.hidden = false; 19090 suggest.classList.add('open', 'trending-mode'); 19091 suggest.style.display = 'block'; 19092 19093 console.log('[Trending] Rendered', trends?.length || 0, 'searches and', products?.length || 0, 'products'); 19094 } 19095 19096 function trackSearchClick(query, productId) { 19097 fetch('/api/track_search_click.php', { 19098 method: 'POST', 19099 headers: { 'Content-Type': 'application/json' }, 19100 body: JSON.stringify({ 19101 query: query, 19102 product_id: productId,
19103 click_type: 'suggestion' 19104 }) 19105 }).catch(() => {}); // silent fail 19106 } 19107 19108 function trackTrendingClick(query) { 19109 fetch('/api/track_search_click.php', { 19110 method: 'POST', 19111 headers: { 'Content-Type': 'application/json' }, 19112 body: JSON.stringify({ 19113 query: query, 19114 click_type: 'trending' 19115 }) 19116 }).catch(() => {}); 19117 } 19118 19119 function trackTrendingProductClick(productId, query) { 19120 fetch('/api/track_search_click.php', { 19121 method: 'POST', 19122 headers: { 'Content-Type': 'application/json' }, 19123 body: JSON.stringify({ 19124 query: query || 'trending_product', 19125 product_id: productId, 19126 click_type: 'trending_product' 19127 }) 19128 }).catch(() => {}); 19129 } 19130 19131 function trackSearchSubmit(query) { 19132 fetch('/api/track_search_click.php', { 19133 method: 'POST', 19134 headers: { 'Content-Type': 'application/json' }, 19135 body: JSON.stringify({ 19136 query: query, 19137 click_type: 'submit' 19138 }) 19139 }).catch(() => {}); 19140 } 19141 19142 function logFinalSearch(query) { 19143 // Log the final search to analytics (not during typing) 19144 fetch('/api/search_products.php?q=' + encodeURIComponent(query) + '&log=1', { 19145 headers: { 'Accept': 'application/json' } 19146 }).catch(() => {}); 19147 } 19148 19149 // ==================== END TRENDING FUNCTIONS ==================== 19150 19151 function clearSuggest(){ 19152 suggest.innerHTML = ''; 19153 suggest.hidden = true; 19154 suggest.classList.remove('open', 'trending-mode'); 19155 currentIndex = -1; 19156 } 19157 19158 // AI Toggle Handler 19159 aiToggleBtn.addEventListener('click', (e) => { 19160 e.preventDefault(); 19161 e.stopPropagation(); 19162 19163 isAIModeEnabled = !isAIModeEnabled; 19164 19165 if (isAIModeEnabled) { 19166 aiToggleBtn.classList.add('active'); 19167 localStorage.setItem('plazait_ai_search_mode', 'enabled'); 19168 updatePlaceholder(); // Update placeholder text 19169 stopTypingAnimation(); // Stop typing animation in AI mode 19170 19171 // Show AI prompt if there's a query 19172 const query = searchInput.value.trim(); 19173 if (query.length >= MIN_LEN) { 19174 showAISearchPrompt(query); 19175 } else { 19176 showAIActivationMessage(); 19177 } 19178 } else { 19179 aiToggleBtn.classList.remove('active'); 19180 localStorage.setItem('plazait_ai_search_mode', 'disabled'); 19181 updatePlaceholder(); // Update placeholder text 19182 startTypingAnimation(); // Start typing animation in normal mode 19183 clearSuggest(); 19184 19185 // Switch to normal search if there's a query 19186 const query = searchInput.value.trim(); 19187 if (query.length >= MIN_LEN) { 19188 performSearch(query); 19189 } 19190 } 19191 }); 19192 19193 function showAIActivationMessage() { 19194 suggest.innerHTML = ` 19195 <div class="ai-progress-container"> 19196 <div class="ai-progress-header"> 19197 <div class="ai-progress-icon">â¨</div> 19198 <div class="ai-progress-text"> 19199 <div class="ai-progress-title">AI Search Mode Activated!</div> 19200 <div class="ai-progress-subtitle">Ketik query Anda untuk pencarian cerdas</div> 19201 </div> 19202 </div> 19203 </div> 19204 `; 19205 suggest.hidden = false; 19206 suggest.classList.add('open'); 19207 suggest.style.display = 'block'; 19208 } 19209 19210 function renderAIInsights(insights){ 19211 if (!insights) return ''; 19212 19213 // Generate AI summary via backend endpoint 19214 const summary = insights.ai_summary || generateAISummary(insights); 19215 19216 if (!summary) return ''; 19217 19218 return ` 19219 <div class="ai-result-banner"> 19220 <div class="ai-result-icon">â¨</div> 19221 <div class="ai-result-text"> 19222 <div class="ai-result-title">AI Assistant</div> 19223 <div class="ai-result-description">${summary}</div> 19224 </div> 19225 </div> 19226 `; 19227 } 19228 19229 function generateAISummary(insights) { 19230 // Fallback client-side summary generation if backend doesn't provide ai_summary 19231 const productName = insights.product_name || ''; 19232 const categoryPath = insights.category_path || ''; 19233 const priceRange = insights.price_range || {}; 19234 const intent = insights.intent || ''; 19235 19236 let summary = ''; 19237 19238 // Parse category for natural display 19239 let categoryDisplay = ''; 19240 if (categoryPath) { 19241 const categoryParts = categoryPath.split('/');
19242 categoryDisplay = categoryParts[categoryParts.length - 1] || categoryParts[categoryParts.length - 2] || categoryPath; 19243 } 19244 19245 // Intent-based opening 19246 if (intent === 'cek_stok') { 19247 summary = `Berikut ketersediaan stok untuk <strong>${esc(productName)}</strong>. `; 19248 } else if (intent === 'rekomendasi_produk') { 19249 summary = `Berikut rekomendasi produk <strong>${esc(categoryDisplay || productName)}</strong> yang kami pilihkan untuk Anda. `; 19250 } else { 19251 // Default: cari_harga or general 19252 if (productName && categoryDisplay) { 19253 summary = `Kami menemukan <strong>${esc(productName)}</strong> di kategori ${esc(categoryDisplay)}`; 19254 } else if (productName) { 19255 summary = `Kami menemukan produk <strong>${esc(productName)}</strong>`; 19256 } else if (categoryDisplay) { 19257 summary = `Kami menemukan produk ${esc(categoryDisplay)}`; 19258 } 19259 19260 // Add price context 19261 if (priceRange.min > 0 && priceRange.max > 0) { 19262 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19263 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19264 summary += ` dengan kisaran harga ${minPrice} - ${maxPrice}`; 19265 } else if (priceRange.min > 0) { 19266 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19267 summary += ` dengan harga mulai ${minPrice}`; 19268 } else if (priceRange.max > 0) { 19269 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19270 summary += ` dengan harga maksimal ${maxPrice}`; 19271 } 19272 19273 summary += ' yang sesuai dengan kebutuhan Anda.'; 19274 } 19275 19276 return summary || esc(insights.explanation || ''); 19277 } 19278 19279 function showAIProgressSteps() { 19280 return ` 19281 <div class="ai-progress-container"> 19282 <div class="ai-progress-header"> 19283 <div class="ai-progress-icon">ð¤</div> 19284 <div class="ai-progress-text"> 19285 <div class="ai-progress-title">AI sedang menganalisis...</div> 19286 <div class="ai-progress-subtitle">Memproses query Anda dengan kecerdasan buatan</div> 19287 </div> 19288 </div> 19289 <div class="ai-progress-steps"> 19290 <div class="ai-progress-step active"> 19291 <div class="ai-step-icon">1</div> 19292 <div class="ai-step-text">Memahami maksud pencarian...</div> 19293 </div> 19294 <div class="ai-progress-step pending"> 19295 <div class="ai-step-icon">2</div> 19296 <div class="ai-step-text">Mengekstrak parameter pencarian...</div> 19297 </div> 19298 <div class="ai-progress-step pending"> 19299 <div class="ai-step-icon">3</div> 19300 <div class="ai-step-text">Mencari produk yang relevan...</div> 19301 </div> 19302 </div> 19303 </div> 19304 `; 19305 } 19306 19307 function updateAIProgressStep(step) { 19308 const steps = suggest.querySelectorAll('.ai-progress-step'); 19309 19310 steps.forEach((stepEl, idx) => { 19311 const icon = stepEl.querySelector('.ai-step-icon'); 19312 19313 if (idx < step) { 19314 stepEl.classList.remove('active', 'pending'); 19315 stepEl.classList.add('completed'); 19316 icon.textContent = 'â'; 19317 } else if (idx === step) { 19318 stepEl.classList.remove('pending', 'completed'); 19319 stepEl.classList.add('active'); 19320 } 19321 }); 19322 } 19323
19324 function renderSuggest(items, aiInsights){ 19325 if (!items.length){ 19326 clearSuggest(); 19327 return; 19328 } 19329 19330 let html = ''; 19331 19332 // Show AI insights if available 19333 if (aiInsights && aiInsights.explanation) { 19334 html += renderAIInsights(aiInsights); 19335 } 19336 19337 // Section header for products with hint 19338 html += ` 19339 <div class="suggest-section-header"> 19340 <div class="suggest-header-main"> 19341 <span class="suggest-header-icon">ð¦</span> 19342 <span class="suggest-header-text">Produk yang ditemukan (${items.length})</span> 19343 </div> 19344 <div class="suggest-header-hint">Tekan <kbd>Enter</kbd> untuk lihat semua hasil</div> 19345 </div> 19346 `; 19347 19348 // Render product items 19349 html += items.map((it, idx)=>{ 19350 // Normalisasi & fallback 19351 const id = it.id != null ? it.id : ''; 19352 const name = esc(it.name); 19353 const slug = esc(it.slug != null ? it.slug : it.id); 19354 const img = esc(it.image || '/assets//img/no-image.png'); 19355 const price = esc(it.price_label || ''); 19356 19357 const url = '/product/' + encodeURIComponent(slug); 19358 19359 return ` 19360 <div class="suggest-item" data-index="${idx}" data-url="${url}" data-id="${esc(id)}"> 19361 <img src="${img}" alt="${name}" class="suggest-thumb"> 19362 <div class="suggest-meta"> 19363 <div class="suggest-name">${name}</div> 19364 <div class="suggest-price">${price}</div> 19365 </div> 19366 </div> 19367 `; 19368 }).join(''); 19369 19370 // Footer hint for more results - only show if 10 items (means there might be more) 19371 if (items.length >= 10) { 19372 html += ` 19373 <div class="suggest-footer-hint"> 19374 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: -3px; margin-right: 6px;"> 19375 <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"/> 19376 </svg> 19377 <span>Lihat hasil lengkap dengan menekan <kbd>Enter</kbd> atau <a href="#" class="suggest-footer-link" id="footerSearchLink">klik di sini</a></span> 19378 </div> 19379 `; 19380 } 19381 19382 suggest.innerHTML = html; 19383 suggest.hidden = false; 19384 suggest.classList.add('open'); 19385 suggest.style.display = 'block'; 19386 suggest.style.visibility = 'visible'; 19387 suggest.style.opacity = '1'; 19388 } 19389 19390 function fetchRemote(q, shouldLog = false){ 19391 // Cancel previous request if any 19392 if (currentAbortController) { 19393 currentAbortController.abort(); 19394 } 19395 currentAbortController = new AbortController(); 19396 19397 // Always use standard search for normal mode 19398 // Only add log=1 parameter when shouldLog is true (intentional search) 19399 const logParam = shouldLog ? '&log=1' : ''; 19400 const url = '/api/search_products.php?q=' + encodeURIComponent(q) + logParam; 19401 const fetchOptions = { 19402 headers: {'Accept':'application/json'}, 19403 signal: currentAbortController.signal 19404 }; 19405 19406 return fetch(url, fetchOptions) 19407 .then(r=>r.json()) 19408 .then(j=>{ 19409 if (j.ok && Array.isArray(j.data)) { 19410 return j; 19411 } 19412 return { ok: false, data: [] }; 19413 }) 19414 .catch(err=>{ 19415 if (err.name === 'AbortError') { 19416 return { ok: false, data: [] }; 19417 } 19418 return { ok:false, data:[] }; 19419 }); 19420 } 19421 19422 function showNoResults(q, aiInsights) { 19423 suggest.innerHTML = ` 19424 <div style="padding: 32px 24px; text-align: center; color: #64748b;"> 19425 <div style="font-size: 56px; margin-bottom: 16px; opacity: 0.6;">ð</div> 19426 <div style="font-size: 16px; font-weight: 600; color: #334155; margin-bottom: 8px;"> 19427 Tidak ada hasil untuk "${esc(q)}" 19428 </div> 19429 ${aiInsights && aiInsights.explanation ? ` 19430 <div style="font-size: 13px; margin: 16px auto; padding: 12px 16px; background: #f1f5f9; border-radius: 8px; max-width: 400px; color: #475569;"> 19431 <strong style="color: #EAB308;">ð¤ AI memahami:</strong><br> 19432 ${esc(aiInsights.explanation)} 19433 </div> 19434 ` : ''} 19435 <div style="font-size: 13px; margin-top: 16px; opacity: 0.8;"> 19436 Coba kata kunci lain atau hubungi kami untuk bantuan 19437 </div> 19438 </div> 19439 `; 19440 suggest.hidden = false; 19441 suggest.classList.add('open'); 19442 suggest.style.display = 'block'; 19443 } 19444 19445 function showErrorMessage() { 19446 suggest.innerHTML = ` 19447 <div style="padding: 32px 24px; text-align: center; color: #dc2626;"> 19448 <div style="font-size: 48px; margin-bottom: 12px;">â ï¸</div> 19449 <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;"> 19450 Pencarian tidak tersedia 19451 </div> 19452 <div style="font-size: 12px; opacity: 0.8;">
19453 Silakan coba lagi dalam beberapa saat 19454 </div> 19455 </div> 19456 `; 19457 suggest.hidden = false; 19458 suggest.classList.add('open'); 19459 suggest.style.display = 'block'; 19460 } 19461 19462 function showAIErrorState(q) { 19463 suggest.innerHTML = ` 19464 <div class="ai-progress-container"> 19465 <div class="ai-progress-header"> 19466 <div class="ai-progress-icon" style="background: linear-gradient(135deg, #CA8A04 0%, #CA8A04 100%);">â¡</div> 19467 <div class="ai-progress-text"> 19468 <div class="ai-progress-title">AI sedang memproses...</div> 19469 <div class="ai-progress-subtitle">Menganalisis query: "${esc(q)}"</div> 19470 </div> 19471 </div> 19472 <div style="padding: 16px; margin-top: 12px; background: #FEF3C7; border-radius: 8px; border-left: 3px solid #CA8A04;"> 19473 <div style="font-size: 13px; color: #8B6914; line-height: 1.6;"> 19474 <strong style="display: block; margin-bottom: 4px;">ð¡ Sedang mencari produk terbaik untuk Anda</strong> 19475 AI kami sedang menganalisis database untuk menemukan hasil yang paling relevan. 19476 Mohon tunggu sebentar... 19477 </div> 19478 </div> 19479 <div style="padding: 12px 16px; margin-top: 12px; text-align: center;"> 19480 <button onclick="location.reload()" style="padding: 8px 16px; background: #FBBF24; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;"> 19481 ð Refresh & Coba Lagi 19482 </button> 19483 </div> 19484 </div> 19485 `; 19486 suggest.hidden = false; 19487 suggest.classList.add('open'); 19488 suggest.style.display = 'block'; 19489 } 19490 19491 function triggerAISearch(q) { 19492 lastQuery = q; // Update lastQuery to prevent re-triggering 19493 isAIProcessing = true; // Set flag to prevent suggest box from closing 19494 19495 // Show AI progress UI - keep suggest box visible 19496 suggest.innerHTML = showAIProgressSteps(); 19497 suggest.hidden = false; 19498 suggest.classList.add('open'); 19499 suggest.style.display = 'block'; 19500 19501 setTimeout(() => updateAIProgressStep(1), 1000); 19502 setTimeout(() => updateAIProgressStep(2), 2500); 19503 19504 // Cancel previous request if any 19505 if (currentAbortController) { 19506 currentAbortController.abort(); 19507 } 19508 currentAbortController = new AbortController(); 19509 19510 const url = '/api/search_ai.php?q=' + encodeURIComponent(q); 19511 const fetchOptions = { 19512 headers: {'Accept':'application/json'}, 19513 signal: currentAbortController.signal 19514 }; 19515 19516 // Add timeout 19517 const timeoutId = setTimeout(() => { 19518 currentAbortController.abort(); 19519 19520 // Show fallback loading state 19521 suggest.innerHTML = ` 19522 <div class="ai-progress-container"> 19523 <div class="ai-progress-header"> 19524 <div class="ai-progress-icon">â³</div> 19525 <div class="ai-progress-text"> 19526 <div class="ai-progress-title">AI timeout - switching to standard search</div> 19527 <div class="ai-progress-subtitle">Mencari dengan metode tradisional...</div> 19528 </div> 19529 </div> 19530 </div> 19531 `; 19532 19533 // Fallback to traditional search 19534 fetch('/api/search_products.php?q=' + encodeURIComponent(q), { 19535 headers: { 'Accept': 'application/json' } 19536 }) 19537 .then(r => r.json()) 19538 .then(j => { 19539 isAIProcessing = false; // Reset flag 19540 if (j.ok && Array.isArray(j.data) && j.data.length) { 19541 renderSuggest(j.data, null); 19542 } else { 19543 showNoResults(q, null); 19544 } 19545 }) 19546 .catch(() => { 19547 isAIProcessing = false; // Reset flag 19548 showAIErrorState(q); 19549 }); 19550 }, AI_TIMEOUT); 19551 19552 fetch(url, fetchOptions) 19553 .then(r => r.json()) 19554 .then(j => { 19555 clearTimeout(timeoutId); 19556 isAIProcessing = false; // Reset flag when done 19557 19558 if (j.ok && Array.isArray(j.data)) { 19559 if (j.data.length > 0) { 19560 renderSuggest(j.data, j.ai_insights || null); 19561 } else { 19562 showNoResults(q, j.ai_insights || null); 19563 } 19564 } else { 19565 showAIErrorState(q); 19566 } 19567 }) 19568 .catch(err => { 19569 clearTimeout(timeoutId); 19570 isAIProcessing = false; // Reset flag on error 19571 if (err.name !== 'AbortError') { 19572 showAIErrorState(q); 19573 } 19574 }); 19575 } 19576 19577 function performSearch(q){ 19578 if (q.length < MIN_LEN){ 19579 clearSuggest(); 19580 return; 19581 } 19582 if (q === lastQuery) return; 19583 lastQuery = q; 19584 19585 // Normal mode only - AI mode uses triggerAISearch() 19586 fetchRemote(q).then(j=>{ 19587 if (j.ok && Array.isArray(j.data)){ 19588 if (j.data.length > 0) { 19589 renderSuggest(j.data, null); 19590 } else { 19591 showNoResults(q, null); 19592 } 19593 } else { 19594 clearSuggest(); 19595 } 19596 }).catch(() => { 19597 clearSuggest(); 19598 }); 19599 } 19600 19601 function debounce(){ 19602 const q = searchInput.value.trim(); 19603 clearTimeout(debounceTimer); 19604 19605 // If empty, show trending suggestions and products 19606 if (q.length === 0) { 19607 debounceTimer = setTimeout(async () => { 19608 const trends = trendingCache || await fetchTrending(); 19609 const products = trendingProductsCache || await fetchTrendingProducts(); 19610 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19611 renderTrendingSuggestions(trends, products); 19612 } else { 19613 clearSuggest(); 19614 } 19615 }, 100); 19616 return; 19617 } 19618 19619 // For AI mode, just show search prompt (no auto search) 19620 if (isAIModeEnabled && q.length >= MIN_LEN) { 19621 showAISearchPrompt(q); 19622 } else if (!isAIModeEnabled && q.length >= MIN_LEN) { 19623 // Normal mode: auto search with debounce 19624 debounceTimer = setTimeout(()=> performSearch(q), DEBOUNCE); 19625 } else { 19626 clearSuggest(); 19627 } 19628 } 19629 19630 function showAISearchPrompt(q) { 19631 suggest.innerHTML = ` 19632 <div class="ai-search-prompt"> 19633 <div class="ai-prompt-header"> 19634 <div class="ai-prompt-icon">ð¤</div> 19635 <div class="ai-prompt-text"> 19636 <div class="ai-prompt-title">Pencarian AI Siap!</div> 19637 <div class="ai-prompt-subtitle">Klik tombol di bawah untuk mencari dengan AI</div> 19638 </div> 19639 </div> 19640 <div class="ai-prompt-query"> 19641 <div class="ai-query-label">
19641Query Anda:</div> 19642 <div class="ai-query-text">"${esc(q)}"</div> 19643 </div> 19644 <button class="ai-search-trigger-btn" data-query="${esc(q)}"> 19645 <span class="ai-btn-icon">â¨</span> 19646 <span class="ai-btn-text">Cari dengan AI</span> 19647 </button> 19648 <div class="ai-prompt-footer"> 19649 AI akan menganalisis query Anda untuk hasil yang lebih relevan 19650 </div> 19651 </div> 19652 `; 19653 suggest.hidden = false; 19654 suggest.classList.add('open'); 19655 suggest.style.display = 'block'; 19656 } 19657 19658 function moveHighlight(dir){ 19659 if (suggest.hidden) return; 19660 const items = suggest.querySelectorAll('.suggest-item'); 19661 if (!items.length) return; 19662 currentIndex += dir; 19663 if (currentIndex < 0) currentIndex = items.length - 1; 19664 if (currentIndex >= items.length) currentIndex = 0; 19665 items.forEach(it=> it.classList.remove('active')); 19666 const active = items[currentIndex]; 19667 if (active){ 19668 active.classList.add('active'); 19669 active.scrollIntoView({block:'nearest'}); 19670 } 19671 } 19672 19673 function activate(){ 19674 const active = suggest.querySelector('.suggest-item.active'); 19675 if (!active) return; 19676 window.location.href = active.getAttribute('data-url'); 19677 } 19678 19679 searchInput.addEventListener('input', debounce); 19680 19681 // Stop typing animation when focused & load trending if empty 19682 searchInput.addEventListener('focus', async () => { 19683 stopTypingAnimation(); 19684 19685 const q = searchInput.value.trim(); 19686 19687 // If empty, show trending suggestions and products 19688 if (q.length === 0) { 19689 const trends = trendingCache || await fetchTrending(); 19690 const products = trendingProductsCache || await fetchTrendingProducts(); 19691 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19692 renderTrendingSuggestions(trends, products); 19693 } 19694 return; 19695 } 19696 19697 // If AI mode and has query, show AI prompt 19698 if (isAIModeEnabled && q.length >= MIN_LEN && suggest.hidden) { 19699 showAISearchPrompt(q); 19700 } 19701 }); 19702 19703 // Resume typing animation when blurred (if suggest is closed) 19704 searchInput.addEventListener('blur', () => { 19705 // Add delay to allow click events to fire first 19706 setTimeout(() => { 19707 if (!isAIModeEnabled && suggest.hidden && searchInput.value.trim() === '') { 19708 startTypingAnimation(); 19709 } 19710 }, 200); 19711 }); 19712 19713 searchInput.addEventListener('keydown', e=>{ 19714 switch(e.key){ 19715 case 'ArrowDown': moveHighlight(1); e.preventDefault(); break; 19716 case 'ArrowUp': moveHighlight(-1); e.preventDefault(); break; 19717 case 'Enter': 19718 if (!suggest.hidden){ 19719 activate(); 19720 e.preventDefault(); 19721 } else { 19722 // Direct enter without suggestion - track search submit and log final search 19723 const q = searchInput.value.trim(); 19724 if (q.length >= MIN_LEN) { 19725 trackSearchSubmit(q); 19726 logFinalSearch(q); 19727 } 19728 } 19729 break; 19730 case 'Escape': clearSuggest(); break; 19731 } 19732 }); 19733 19734 suggest.addEventListener('click', e=>{ 19735 // Check if footer link clicked 19736 const footerLink = e.target.closest('#footerSearchLink'); 19737 if (footerLink) { 19738 e.preventDefault(); 19739 const q = searchInput.value.trim(); 19740 if (q.length >= MIN_LEN) { 19741 trackSearchSubmit(q); 19742 logFinalSearch(q); 19743 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19744 } 19745 return; 19746 } 19747 19748 // Check if AI search button clicked 19749 const aiBtn = e.target.closest('.ai-search-trigger-btn'); 19750 if (aiBtn) { 19751 const query = aiBtn.getAttribute('data-query'); 19752 if (query) { 19753 triggerAISearch(query); 19754 } 19755 return; 19756 } 19757 19758 // Handle trending item click 19759 const trendingItem = e.target.closest('.trending-item'); 19760 if (trendingItem) { 19761 const query = trendingItem.getAttribute('data-query'); 19762 if (query) { 19763 searchInput.value = query; 19764 trackTrendingClick(query); 19765 logFinalSearch(query); // Log final search when user clicks trending 19766 performSearch(query); 19767 } 19768 return; 19769 } 19770 19771 // Handle trending product click 19772 const trendingProduct = e.target.closest('.trending-product-item'); 19773 if (trendingProduct) { 19774 const productId = trendingProduct.getAttribute('data-product-id'); 19775 const slug = trendingProduct.getAttribute('data-slug'); 19776 const query = trendingProduct.getAttribute('data-query'); 19777 19778 if (productId && slug) {
19779 trackTrendingProductClick(productId, query); 19780 window.location.href = '/product/' + encodeURIComponent(slug); 19781 } 19782 return; 19783 } 19784 19785 // Handle product item click 19786 const it = e.target.closest('.suggest-item'); 19787 if (!it) return; 19788 19789 const productId = it.getAttribute('data-id'); 19790 trackSearchClick(lastQuery, productId); 19791 logFinalSearch(lastQuery); // Log final search when user clicks on suggestion 19792 19793 window.location.href = it.getAttribute('data-url'); 19794 }); 19795 19796 document.addEventListener('click', e=>{ 19797 // Don't close suggest box if AI is processing 19798 if (isAIProcessing) return; 19799 19800 if (e.target === searchInput || e.target === aiToggleBtn || 19801 suggest.contains(e.target) || aiToggleBtn.contains(e.target)) return; 19802 clearSuggest(); 19803 }); 19804 19805 if (searchBtn){ 19806 searchBtn.addEventListener('click', ()=>{ 19807 const q = searchInput.value.trim(); 19808 if (q.length >= MIN_LEN){ 19809 // Track search submit and log final search 19810 trackSearchSubmit(q); 19811 logFinalSearch(q); 19812 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19813 } 19814 }); 19815 } 19816 19817 // Pastikan style dasar muncul 19818 if (!suggest.classList.contains('wired')) { 19819 suggest.classList.add('wired'); 19820 suggest.style.position = 'absolute'; 19821 suggest.style.zIndex = '9999'; 19822 suggest.style.background = '#fff'; 19823 suggest.style.border = '1px solid #ccc'; 19824 suggest.style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)'; 19825 suggest.style.maxHeight = '360px'; 19826 suggest.style.overflowY = 'auto'; 19827 } 19828 19829 return true; 19830 } 19831 19832 if (!setup()) { 19833 if (document.readyState === 'loading') { 19834 document.addEventListener('DOMContentLoaded', setup); 19835 } else { 19836 setTimeout(setup, 80); 19837 } 19838 } 19839})(); 19840</script>
vendor: 2 bytes, line 19840
19840
19841<script data-inlined="site-notice.js"> 19842/** 19843 * Site Notice Banner - Interactive Behavior 19844 * Handles dismiss functionality with localStorage persistence 19845 * Supports dynamic notice IDs for different campaigns 19846 */ 19847 19848(function() { 19849 'use strict'; 19850 19851 const STORAGE_KEY = 'plazait_site_notice_dismissed'; 19852 19853 const banner = document.getElementById('holidayNotice'); 19854 const closeBtn = document.getElementById('holidayNoticeClose'); 19855 19856 if (!banner || !closeBtn) return; 19857 19858 // Get notice ID from data attribute 19859 const noticeId = banner.dataset.noticeId; 19860 if (!noticeId) { 19861 console.warn('Notice ID not found. Banner will not persist dismiss state.'); 19862 } 19863 19864 /** 19865 * Check if banner was previously dismissed 19866 */ 19867 function wasDismissed() { 19868 if (!noticeId) return false; 19869 19870 try { 19871 const dismissed = localStorage.getItem(STORAGE_KEY); 19872 return dismissed === noticeId; 19873 } catch (e) { 19874 console.warn('localStorage not available:', e); 19875 return false; 19876 } 19877 } 19878 19879 /** 19880 * Mark banner as dismissed 19881 */ 19882 function markDismissed() { 19883 if (!noticeId) return; 19884 19885 try { 19886 localStorage.setItem(STORAGE_KEY, noticeId); 19887 } catch (e) { 19888 console.warn('Could not save to localStorage:', e); 19889 } 19890 } 19891 19892 /** 19893 * Hide banner with animation 19894 */ 19895 function hideBanner() { 19896 banner.classList.add('hidden'); 19897 document.body.classList.remove('has-holiday-notice'); 19898 19899 // Remove from DOM after animation 19900 setTimeout(() => { 19901 banner.remove(); 19902 }, 300); 19903 19904 markDismissed(); 19905 19906 // Analytics tracking (optional) 19907 if (window.gtag) { 19908 window.gtag('event', 'banner_dismiss', { 19909 'event_category': 'Site Notice', 19910 'event_label': noticeId || 'unknown', 19911 }); 19912 } 19913 } 19914 19915 /** 19916 * Initialize banner 19917 */ 19918 function init() { 19919 // Check if already dismissed 19920 if (wasDismissed()) { 19921 banner.remove(); 19922 return; 19923 } 19924 19925 // Add body class for layout adjustment 19926 document.body.classList.add('has-holiday-notice'); 19927 19928 // Close button click 19929 closeBtn.addEventListener('click', hideBanner); 19930 19931 // Keyboard support for close button 19932 closeBtn.addEventListener('keydown', (e) => { 19933 if (e.key === 'Enter' || e.key === ' ') { 19934 e.preventDefault(); 19935 hideBanner(); 19936 } 19937 }); 19938 19939 // Optional: Auto-hide after certain time (disabled by default) 19940 // setTimeout(hideBanner, 30000); // 30 seconds 19941 } 19942 19943 // Initialize when DOM is ready 19944 if (document.readyState === 'loading') { 19945 document.addEventListener('DOMContentLoaded', init); 19946 } else { 19947 init(); 19948 } 19949 19950})(); 19951 19952</script>
vendor: 2 bytes, line 19952
19952
19953<script data-inlined="guest-cart.js"> 19954/** 19955 * Guest Cart Manager - localStorage based cart for guest users 19956 * 19957 * Manages shopping cart items in browser localStorage for users who are not logged in. 19958 * Provides seamless transition to database when user logs in. 19959 */ 19960 19961class GuestCart { 19962 constructor() { 19963 this.storageKey = 'plazait_guest_cart'; 19964 this.apiBase = '/api/cart'; 19965 19966 // Initialize cart structure 19967 this.cart = this.loadFromStorage(); 19968 19969 // Bind methods 19970 this.add = this.add.bind(this); 19971 this.remove = this.remove.bind(this); 19972 this.update = this.update.bind(this); 19973 this.clear = this.clear.bind(this); 19974 this.getItems = this.getItems.bind(this); 19975 this.mergeToDatabase = this.mergeToDatabase.bind(this); 19976 } 19977 19978 /** 19979 * Load cart from localStorage 19980 */ 19981 loadFromStorage() { 19982 try { 19983 const stored = localStorage.getItem(this.storageKey); 19984 if (stored) { 19985 const parsed = JSON.parse(stored); 19986 if (parsed && typeof parsed === 'object' && parsed.items) { 19987 return parsed; 19988 } 19989 } 19990 } catch (error) { 19991 // Silent error handling 19992 } 19993 19994 return { 19995 items: {}, 19996 updated_at: Date.now(), 19997 version: '1.0' 19998 }; 19999 } 20000 20001 /** 20002 * Save cart to localStorage 20003 */ 20004 saveToStorage() { 20005 try { 20006 this.cart.updated_at = Date.now(); 20007 localStorage.setItem(this.storageKey, JSON.stringify(this.cart)); 20008 20009 // Dispatch custom event for cart updates 20010 window.dispatchEvent(new CustomEvent('guestCartUpdated', { 20011 detail: { 20012 itemsCount: this.getItemsCount(), 20013 linesCount: this.getLinesCount(), 20014 items: this.cart.items 20015 } 20016 })); 20017 20018 return true; 20019 } catch (error) { 20020 return false; 20021 } 20022 } 20023 20024 /** 20025 * Generate cart item key 20026 */ 20027 generateKey(productId, variantId = 0) { 20028 return `p${productId}v${variantId}`; 20029 } 20030 20031 /** 20032 * Add item to guest cart 20033 */ 20034 add(productId, qty = 1, variantId = 0, meta = {}) { 20035 const key = this.generateKey(productId, variantId); 20036 20037 // Validate inputs 20038 if (!productId || productId <= 0 || !qty || qty <= 0) { 20039 throw new Error('Invalid product ID or quantity'); 20040 } 20041 20042 qty = Math.max(1, Math.min(999, parseInt(qty))); 20043 20044 // Initialize item if not exists 20045 if (!this.cart.items[key]) { 20046 this.cart.items[key] = { 20047 pid: parseInt(productId), 20048 vid: parseInt(variantId) || 0, 20049 qty: 0, 20050 name: meta.name || '', 20051 sku: meta.sku || '', 20052 price: parseInt(meta.price) || 0, 20053 image_url: meta.image_url || '', 20054 product_url: meta.product_url || '/product/' + parseInt(productId), 20055 added_at: Date.now() 20056 }; 20057 } 20058 20059 // Update quantity 20060 this.cart.items[key].qty = Math.max(1, Math.min(999, this.cart.items[key].qty + qty)); 20061 20062 // Update metadata if provided 20063 if (meta.name) this.cart.items[key].name = meta.name; 20064 if (meta.sku) this.cart.items[key].sku = meta.sku; 20065 if (meta.price) this.cart.items[key].price = parseInt(meta.price); 20066 if (meta.image_url) this.cart.items[key].image_url = meta.image_url; 20067 if (meta.product_url) this.cart.items[key].product_url = meta.product_url; 20068 20069 this.saveToStorage(); 20070 20071 return { 20072 key, 20073 item: this.cart.items[key], 20074 itemsCount: this.getItemsCount(), 20075 linesCount: this.getLinesCount() 20076 }; 20077 } 20078 20079 /** 20080 * Remove item from guest cart 20081 */ 20082 remove(productId, variantId = 0) {
20083 const key = this.generateKey(productId, variantId); 20084 20085 if (this.cart.items[key]) { 20086 delete this.cart.items[key]; 20087 this.saveToStorage(); 20088 20089 return true; 20090 } 20091 20092 return false; 20093 } 20094 20095 /** 20096 * Update item quantity 20097 */ 20098 update(productId, qty, variantId = 0) { 20099 const key = this.generateKey(productId, variantId); 20100 20101 if (!this.cart.items[key]) { 20102 return false; 20103 } 20104 20105 qty = parseInt(qty); 20106 if (qty <= 0) { 20107 return this.remove(productId, variantId); 20108 } 20109 20110 this.cart.items[key].qty = Math.max(1, Math.min(999, qty)); 20111 this.saveToStorage(); 20112 20113 return true; 20114 } 20115 20116 /** 20117 * Clear entire cart 20118 */ 20119 clear() { 20120 this.cart = { 20121 items: {}, 20122 updated_at: Date.now(), 20123 version: '1.0' 20124 }; 20125 20126 this.saveToStorage(); 20127 20128 return true; 20129 } 20130 20131 /** 20132 * Get all cart items 20133 */ 20134 getItems() { 20135 return this.cart.items; 20136 } 20137 20138 /** 20139 * Get cart items as array 20140 */ 20141 getItemsArray() { 20142 return Object.values(this.cart.items); 20143 } 20144 20145 /** 20146 * Get total items count (sum of quantities) 20147 */ 20148 getItemsCount() { 20149 return Object.values(this.cart.items).reduce((total, item) => total + (item.qty || 0), 0); 20150 } 20151 20152 /** 20153 * Get lines count (number of different items) 20154 */ 20155 getLinesCount() { 20156 return Object.keys(this.cart.items).length; 20157 } 20158 20159 /** 20160 * Get cart summary 20161 */ 20162 getSummary() { 20163 const itemsCount = this.getItemsCount(); 20164 const linesCount = this.getLinesCount(); 20165 const totalValue = Object.values(this.cart.items).reduce( 20166 (total, item) => total + ((item.qty || 0) * (item.price || 0)), 0 20167 ); 20168 20169 return { 20170 itemsCount, 20171 linesCount, 20172 totalValue, 20173 updatedAt: this.cart.updated_at 20174 }; 20175 } 20176 20177 /** 20178 * Check if cart has items 20179 */ 20180 isEmpty() { 20181 return this.getLinesCount() === 0; 20182 } 20183 20184 /** 20185 * Merge guest cart to database after user login 20186 */ 20187 async mergeToDatabase() { 20188 const items = this.cart.items; 20189 20190 if (Object.keys(items).length === 0) { 20191 return { ok: true, merged_count: 0, message: 'No items to merge' }; 20192 } 20193 20194 try { 20195 // Use new endpoint that handles localStorage format 20196 const response = await fetch(`${this.apiBase}/merge-localStorage.php`, { 20197 method: 'POST', 20198 headers: { 20199 'Content-Type': 'application/json', 20200 }, 20201 credentials: 'same-origin', 20202 body: JSON.stringify({ 20203 items: items // Send localStorage format directly 20204 }) 20205 }); 20206 20207 const result = await response.json(); 20208 20209 if (result.ok) { 20210 // Clear localStorage cart after successful merge 20211 this.clear(); 20212 20213 // Update cart counter 20214 if (window.updateCartCounter) { 20215 try { 20216 await window.updateCartCounter(); 20217 } catch (e) { 20218 // Silent error handling 20219 } 20220 } 20221 20222 // Dispatch merge completion event 20223 window.dispatchEvent(new CustomEvent('guestCartMerged', { 20224 detail: result 20225 })); 20226 20227 return result; 20228 } else { 20229 throw new Error(result.error || 'Failed to merge cart'); 20230 } 20231 20232 } catch (error) { 20233 throw error; 20234 } 20235 } 20236 20237 /** 20238 * Export cart data (for debugging or backup) 20239 */ 20240 export() { 20241 return { 20242 ...this.cart,
20243 summary: this.getSummary() 20244 }; 20245 } 20246 20247 /** 20248 * Import cart data (for restore or testing) 20249 */ 20250 import(cartData) { 20251 if (cartData && typeof cartData === 'object' && cartData.items) { 20252 this.cart = { 20253 items: cartData.items, 20254 updated_at: cartData.updated_at || Date.now(), 20255 version: cartData.version || '1.0' 20256 }; 20257 20258 this.saveToStorage(); 20259 return true; 20260 } 20261 20262 return false; 20263 } 20264} 20265 20266// Create global instance 20267window.guestCart = new GuestCart(); 20268 20269// Auto-merge on page load if user is logged in 20270document.addEventListener('DOMContentLoaded', async function() { 20271 // Multiple ways to check if user is logged in 20272 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20273 document.body.classList.contains('logged-in') || 20274 window.userLoggedIn === true || 20275 document.body.dataset.userLoggedIn === 'true'; 20276 20277 if (isLoggedIn && !window.guestCart.isEmpty()) { 20278 // Small delay to ensure page is fully loaded 20279 setTimeout(async () => { 20280 try { 20281 const result = await window.guestCart.mergeToDatabase(); 20282 } catch (error) { 20283 // Silent error handling 20284 } 20285 }, 500); 20286 } 20287}); 20288 20289// Also listen for login events (if login happens via AJAX) 20290window.addEventListener('userLoggedIn', async function() { 20291 if (!window.guestCart.isEmpty()) { 20292 try { 20293 await window.guestCart.mergeToDatabase(); 20294 } catch (error) { 20295 // Silent error handling 20296 } 20297 } 20298}); 20299</script>
vendor: 2 bytes, line 20299
20299
20300<script data-inlined="cart.js"> 20301(function(){ 20302 function $(s,ctx=document){ return ctx.querySelector(s); } 20303 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 20304 20305 // CSRF token helper 20306 function getCsrfToken() { 20307 const metaTag = document.querySelector('meta[name="csrf-token"]'); 20308 return metaTag ? metaTag.getAttribute('content') : ''; 20309 } 20310 20311 function guessPdMeta(){ 20312 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 20313 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 20314 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 20315 const imgEl = $('#pd-main-img'); 20316 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 20317 return { name, sku, image_url }; // price removed for security 20318 } 20319 20320 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 20321 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 20322 const payload = { 20323 product_id: parseInt(productId||0,10) || 0, 20324 qty: parseInt(qty||1,10) || 1, 20325 variant_id: parseInt(variantId||0,10) || 0, 20326 meta 20327 }; 20328 20329 // Check if user is logged in 20330 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20331 document.body.classList.contains('logged-in') || 20332 window.userLoggedIn === true || 20333 document.body.dataset.userLoggedIn === 'true'; 20334 20335 // Skip localStorage untuk logged user - mereka pakai database 20336 if (!isLoggedIn && window.guestCart) { 20337 try { 20338 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 20339 } catch (e) { 20340 console.warn('Failed to add to localStorage cart:', e); 20341 } 20342 } 20343 20344 const res = await fetch('/api/cart/add.php', { 20345 method: 'POST', 20346 headers: { 20347 'Content-Type':'application/json', 20348 'X-CSRF-Token': getCsrfToken() 20349 }, 20350 credentials: 'same-origin', 20351 body: JSON.stringify({ 20352 ...payload, 20353 csrf_token: getCsrfToken() 20354 }), 20355 }); 20356 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 20357 if (!res.ok || !data.ok) { 20358 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 20359 throw new Error(msg); 20360 } 20361 20362 // Update badge di navbar dengan data dari response 20363 try { 20364 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20365 if (el && data.cart && typeof data.cart.items_count === 'number') { 20366 el.textContent = String(data.cart.items_count); 20367 el.classList.toggle('is-empty', data.cart.items_count <= 0); 20368 } 20369 } catch(_){} 20370 20371 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 20372 try { 20373 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 20374 } catch(_){} 20375 20376 // Update CartBadge dengan data dari response (lebih reliable) 20377 try { 20378 if (window.CartBadge && window.CartBadge.set) { 20379 window.CartBadge.set(data.cart.items_count||0); 20380 } 20381 } catch(_){} 20382 20383 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 20384 setTimeout(() => { 20385 if (window.updateCartCounter) { 20386 window.updateCartCounter(); 20387 } 20388 }, 100); 20389 20390 return data; 20391 } 20392 20393 // Function to update cart counter from server 20394 async function updateCartCounter(){ 20395 try { 20396 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 20397 const data = await res.json().catch(()=>({ok:false})); 20398 if (data.ok && typeof data.items_count === 'number') { 20399 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20400 if (el) { 20401 el.textContent = String(data.items_count); 20402 el.classList.toggle('is-empty', data.items_count <= 0); 20403 } 20404 // Dispatch event 20405 try { 20406 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 20407 } catch(_){} 20408 } 20409 } catch (err) { 20410 console.log('Could not update cart counter:', err); 20411 } 20412 } 20413 20414 // Function to manually merge guest cart (if needed) 20415 async function mergeGuestCart(){ 20416 try { 20417 const res = await fetch('/api/cart/merge-guest.php', { 20418 method: 'POST', 20419 headers: { 20420 'Content-Type': 'application/json', 20421 'X-CSRF-Token': getCsrfToken() 20422 }, 20423 credentials: 'same-origin', 20424 body: JSON.stringify({ csrf_token: getCsrfToken() }) 20425 }); 20426 const data = await res.json().catch(()=>({ok:false})); 20427 20428 if (data.ok && data.merged_items > 0) { 20429 console.log(`Guest cart merged: ${data.merged_items} items`); 20430 // Update cart counter after merge 20431 await updateCartCounter(); 20432 return data; 20433 } 20434 return data; 20435 } catch (err) { 20436 console.log('Could not merge guest cart:', err); 20437 return { ok: false, error: err.message }; 20438 } 20439 } 20440 20441 window.addToCart = addToCart; 20442 window.updateCartCounter = updateCartCounter; 20443 window.mergeGuestCart = mergeGuestCart; 20444 20445 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 20446 document.addEventListener('click', async (e)=>{ 20447 const btn = e.target.closest('[data-addtocart]'); 20448 if (!btn) return; 20449 e.preventDefault(); 20450 if (btn.disabled) return; 20451 20452 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 20453 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
20454 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 20455 20456 try { 20457 const data = await addToCart(pid, qty, vid); 20458 if (window.showToast) { 20459 const meta = guessPdMeta(); 20460 window.showToast({ 20461 type:'success', 20462 title:'Berhasil ditambahkan', 20463 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 20464 duration: 3800, 20465 actions: [ 20466 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 20467 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 20468 ] 20469 }); 20470 } 20471 } catch (err) { 20472 if (window.showToast) { 20473 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 20474 } else { 20475 console.error(err); 20476 } 20477 } 20478 }); 20479})(); 20480</script>
vendor: 2 bytes, line 20480
20480
20481<script data-inlined="mega-menu-tiles.js"> 20482// Tile Mega Menu â Enhanced 2025 (Stagger animations, product counts, loading states) 20483// Includes safe-area hover handling (no backdrop flicker) 20484 20485(function(){ 20486 const trigger = document.querySelector('[data-mm2="trigger"]'); 20487 const panel = document.querySelector('[data-mm2="panel"]'); 20488 const topView = document.querySelector('[data-mm2="top"]'); 20489 const detailView= document.querySelector('[data-mm2="detail"]'); 20490 const backdrop = document.querySelector('[data-mm2="backdrop"]'); 20491 if (!trigger || !panel || !topView || !detailView) return; 20492 20493 const apiUrl = '/api/categories_tree.php?top=16&child=18&grand=14'; 20494 const isDesktop = () => window.matchMedia('(min-width: 961px)').matches; 20495 20496 let data = []; 20497 let hoverTimer, closeTimer; 20498 let isLoading = false; 20499 20500 const esc = s => (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 20501 20502 // Mobile close button HTML 20503 const mobileCloseBtn = () => ` 20504 <button type="button" class="mm2-mobile-close" data-mm2="close" aria-label="Tutup menu"> 20505 <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"> 20506 <line x1="18" y1="6" x2="6" y2="18"></line> 20507 <line x1="6" y1="6" x2="18" y2="18"></line> 20508 </svg> 20509 </button> 20510 `; 20511 20512 // Category-specific gradient colors (16 unique combinations) 20513 const categoryGradients = [ 20514 { from: '#e0e7ff', to: '#fff', border: '#c7d2fe', color: '#78350F' }, // Indigo 20515 { from: '#FEF3C7', to: '#fff', border: '#FDE68A', color: '#8B6914' }, // Blue 20516 { from: '#d1fae5', to: '#fff', border: '#a7f3d0', color: '#059669' }, // Green 20517 { from: '#FEF3C7', to: '#fff', border: '#FDE68A', color: '#B45309' }, // Amber 20518 { from: '#fce7f3', to: '#fff', border: '#fbcfe8', color: '#db2777' }, // Pink 20519 { from: '#FEF08A', to: '#fff', border: '#FBBF24', color: '#B45309' }, // Purple 20520 { from: '#FEF3C7', to: '#fff', border: '#FDE68A', color: '#CA8A04' }, // Orange 20521 { from: '#ccfbf1', to: '#fff', border: '#99f6e4', color: '#0d9488' }, // Teal 20522 { from: '#FEF3C7', to: '#fff', border: '#FDE68A', color: '#B45309' }, // Yellow 20523 { from: '#ffe4e6', to: '#fff', border: '#fecdd3', color: '#e11d48' }, // Rose 20524 { from: '#FEF9C3', to: '#fff', border: '#FDE68A', color: '#A16207' }, // Gold 20525 { from: '#FFFBEB', to: '#fff', border: '#FEF08A', color: '#EAB308' }, // Violet 20526 { from: '#dcfce7', to: '#fff', border: '#bbf7d0', color: '#16a34a' }, // Emerald 20527 { from: '#fef2f2', to: '#fff', border: '#fecaca', color: '#dc2626' }, // Red 20528 { from: '#FFFBEB', to: '#fff', border: '#FEF3C7', color: '#CA8A04' }, // Gold-600 20529 { from: '#FFFBEB', to: '#fff', border: '#FEF3C7', color: '#8B6914' }, // Violet-600 20530 ]; 20531 20532 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>`; 20533 20534 // Recent (optional future use) 20535 const REC_KEY='mm2_recent'; 20536 const saveRecent=(path,label)=>{ try{ 20537 let list=JSON.parse(localStorage.getItem(REC_KEY)||'[]').filter(x=>x&&x.path!==path); 20538 list.unshift({path,label,t:Date.now()}); localStorage.setItem(REC_KEY, JSON.stringify(list.slice(0,8))); 20539 }catch{} }; 20540 20541 function isToSafeArea(e){
20542 const t = e && e.relatedTarget ? e.relatedTarget : null; 20543 if (!t) return false; 20544 return (trigger.contains(t) || panel.contains(t) || (backdrop && backdrop.contains(t))); 20545 } 20546 20547 function open(){ 20548 document.body.classList.add('mm2-open'); 20549 showTop(); 20550 // Add entrance animation class 20551 setTimeout(() => panel.classList.add('mm2-animated'), 10); 20552 } 20553 function close(){ 20554 document.body.classList.remove('mm2-open'); 20555 panel.classList.remove('mm2-animated'); 20556 } 20557 function showTop(){ 20558 topView.classList.add('is-active'); 20559 detailView.classList.remove('is-active'); 20560 } 20561 20562 // Loading skeleton 20563 function showLoading() { 20564 const skeletons = Array.from({length: 12}, (_, i) => ` 20565 <div class="mm2-card mm2-skeleton" style="animation-delay: ${i * 0.02}s"> 20566 <span class="mm2-ico skeleton-box"></span> 20567 <span style="flex: 1;"> 20568 <div class="skeleton-line" style="width: 60%; height: 16px; margin-bottom: 6px;"></div> 20569 <div class="skeleton-line" style="width: 40%; height: 12px;"></div> 20570 </span> 20571 </div> 20572 `).join(''); 20573 topView.innerHTML = `<div class="mm2-top"><div class="mm2-grid">${skeletons}</div></div>`; 20574 } 20575 20576 // Count total products in a category tree 20577 function countProducts(node) { 20578 let count = node.product_count || 0; 20579 if (Array.isArray(node.children)) { 20580 node.children.forEach(child => { 20581 count += countProducts(child); 20582 }); 20583 } 20584 return count; 20585 } 20586 20587 // Cache for fetched thumbnails to avoid duplicate requests 20588 const thumbnailCache = new Map(); 20589 20590 // Fetch product thumbnails for a category 20591 function fetchCategoryThumbnails(categoryPath) { 20592 // Check cache first 20593 if (thumbnailCache.has(categoryPath)) { 20594 injectThumbnails(categoryPath, thumbnailCache.get(categoryPath)); 20595 return; 20596 } 20597 20598 const url = `/api/category_products_preview.php?path=${encodeURIComponent(categoryPath)}&limit=2`; 20599 20600 fetch(url) 20601 .then(r => { 20602 if (!r.ok) throw new Error('Network error'); 20603 return r.json(); 20604 }) 20605 .then(data => { 20606 if (data.success && data.products && data.products.length > 0) { 20607 thumbnailCache.set(categoryPath, data.products); 20608 injectThumbnails(categoryPath, data.products); 20609 } else { 20610 // No products, remove skeleton 20611 removeThumbSkeletons(categoryPath); 20612 } 20613 }) 20614 .catch(err => { 20615 console.warn('Failed to load thumbnails for', categoryPath, err); 20616 removeThumbSkeletons(categoryPath); 20617 }); 20618 } 20619 20620 // Inject thumbnails into the DOM 20621 function injectThumbnails(categoryPath, products) { 20622 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20623 links.forEach(link => { 20624 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20625 if (!thumbsContainer) return; 20626 20627 const thumbsHtml = products.slice(0, 2).map(p => 20628 `<img src="${esc(p.img)}" alt="${esc(p.name)}" class="mm2-thumb" title="${esc(p.name)}" loading="lazy">` 20629 ).join(''); 20630 20631 thumbsContainer.innerHTML = thumbsHtml; 20632 thumbsContainer.removeAttribute('data-loading'); 20633 }); 20634 } 20635 20636 // Remove skeleton loaders when no products 20637 function removeThumbSkeletons(categoryPath) { 20638 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20639 links.forEach(link => { 20640 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20641 if (thumbsContainer) { 20642 thumbsContainer.remove(); 20643 } 20644 }); 20645 } 20646 20647 function showDetail(node){ 20648 const title = node.label.split('/')[0]; 20649 const children = Array.isArray(node.children) ? node.children : []; 20650 const hasChildren = children.length > 0; 20651 20652 const subnav = hasChildren ? ` 20653 <nav class="mm2-subnav" role="tablist"> 20654 ${children.map((c2,i)=>{ 20655 const t = c2.label.split('/').slice(-1)[0]; 20656 return `<button class="mm2-subitem${i===0?' active':''}" role="tab" data-idx="${i}" aria-selected="${i===0?'true':'false'}">${esc(t)}</button>`; 20657 }).join('')} 20658 </nav>` : ''; 20659 20660 const subcontent = `<div class="mm2-subcontent" data-mm2="subcontent"></div>`; 20661
20662 detailView.innerHTML = ` 20663 <div class="mm2-detail"> 20664 <div class="mm2-detail-header"> 20665 <button class="mm2-back" type="button" data-mm2="back" aria-label="Kembali"> 20666 <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> 20667 </button> 20668 <div class="mm2-detail-title">${esc(title)}</div> 20669 </div> 20670 20671 ${hasChildren ? ` 20672 <div class="mm2-detail-layout"> 20673 ${subnav} 20674 ${subcontent} 20675 </div>` : ` 20676 <div class="mm2-links"> 20677 <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)}"> 20678 <strong>Lihat semua ${esc(title)}</strong> 20679 </a> 20680 </div> 20681 `} 20682 </div> 20683 `; 20684 20685 topView.classList.remove('is-active'); 20686 detailView.classList.add('is-active'); 20687 20688 if (hasChildren) { 20689 const subEl = detailView.querySelector('.mm2-subnav'); 20690 const contEl = detailView.querySelector('[data-mm2="subcontent"]'); 20691 let activeIdx = 0; 20692 20693 const renderSubgrid = (idx) => { 20694 const c2 = children[idx]; if (!c2) { contEl.innerHTML=''; return; } 20695 const title2 = c2.label.split('/').slice(-1)[0]; 20696 const l3 = Array.isArray(c2.children) ? c2.children : []; 20697 const l3Html = l3.map(c3 => ` 20698 <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)}"> 20699 <span class="mm2-link-text">${esc(c3.label.split('/').slice(-1)[0])}</span> 20700 <span class="mm2-thumbs" data-loading="true"> 20701 <span class="mm2-thumb-skeleton"></span> 20702 <span class="mm2-thumb-skeleton"></span> 20703 </span> 20704 </a>`).join(''); 20705 20706 contEl.innerHTML = ` 20707 <div class="mm2-links"> 20708 <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)}"> 20709 <strong>Lihat semua ${esc(title2)}</strong> 20710 </a> 20711 ${l3Html} 20712 </div> 20713 `; 20714 20715 // Fetch product thumbnails for each level-3 category (desktop only) 20716 if (isDesktop()) { 20717 setTimeout(() => { 20718 l3.forEach(c3 => { 20719 fetchCategoryThumbnails(c3.path); 20720 }); 20721 }, 100); 20722 } 20723 }; 20724 20725 renderSubgrid(activeIdx); 20726 20727 // Desktop: hover ganti konten; Mobile & Desktop: click pilih 20728 subEl.addEventListener('mousemove', (e)=>{ 20729 if (!isDesktop()) return; 20730 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20731 const idx = +it.dataset.idx; if (!Number.isInteger(idx) || idx===activeIdx) return; 20732 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20733 it.classList.add('active'); 20734 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20735 activeIdx = idx; 20736 renderSubgrid(activeIdx); 20737 }); 20738 20739 subEl.addEventListener('click', (e)=>{ 20740 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20741 const idx = +it.dataset.idx; if (!Number.isInteger(idx)) return; 20742 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20743 it.classList.add('active'); 20744 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20745 activeIdx = idx; 20746 renderSubgrid(activeIdx); 20747 }); 20748 20749 // Delegate link clicks to add recent 20750 detailView.addEventListener('click', (e)=>{ 20751 const nav = e.target.closest('[data-mm2="nav"]'); 20752 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20753 }); 20754 } 20755 } 20756 20757 function buildTop(){ 20758 const html = data.map((n, idx) => { 20759 const title = n.label.split('/')[0]; 20760 const childCount = (n.children||[]).length;
20761 const productCount = countProducts(n); 20762 const gradient = categoryGradients[idx % categoryGradients.length]; 20763 20764 // Dynamic subtitle based on content - shorter for mobile 20765 let sub = ''; 20766 if (productCount > 0) { 20767 sub = `${productCount} produk`; 20768 if (childCount > 0) sub += ` · ${childCount} kat`; 20769 } else if (childCount > 0) { 20770 sub = `${childCount} kategori`; 20771 } else { 20772 sub = 'Lihat semua'; 20773 } 20774 20775 const iconStyle = `background: linear-gradient(135deg, ${gradient.from}, ${gradient.to}); border-color: ${gradient.border}; color: ${gradient.color};`; 20776 20777 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)}"> 20778 <span class="mm2-ico" style="${iconStyle}">${icon()}</span> 20779 <span style="flex: 1;"> 20780 <div class="mm2-title">${esc(title)}</div> 20781 <div class="mm2-sub">${esc(sub)}</div> 20782 </span> 20783 </a>`; 20784 }).join(''); 20785 const closeBtnHtml = !isDesktop() ? mobileCloseBtn() : ''; 20786 topView.innerHTML = `<div class="mm2-top">${closeBtnHtml}<div class="mm2-grid">${html}</div></div>`; 20787 } 20788 20789 // Open/close with safe-area 20790 trigger.addEventListener('click', (e)=>{ e.preventDefault(); document.body.classList.contains('mm2-open') ? close() : open(); }); 20791 trigger.addEventListener('pointerenter', ()=>{ if (isDesktop()) { clearTimeout(closeTimer); hoverTimer=setTimeout(open, 80); }}); 20792 trigger.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) { clearTimeout(hoverTimer); closeTimer=setTimeout(close, 200); }}); 20793 panel.addEventListener('pointerenter', ()=>{ if (isDesktop()) clearTimeout(closeTimer); }); 20794 panel.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) closeTimer=setTimeout(close, 200); }); 20795 backdrop?.addEventListener('pointerenter', ()=>{ clearTimeout(closeTimer); }); 20796 backdrop?.addEventListener('click', close); 20797 document.addEventListener('keydown', (e)=>{ if (e.key==='Escape') close(); }); 20798 20799 // Card click â open detail (unless cmd/ctrl to open link) 20800 panel.addEventListener('click', (e)=>{ 20801 const closeBtn = e.target.closest('[data-mm2="close"]'); if (closeBtn) { e.preventDefault(); close(); return; } 20802 const back = e.target.closest('[data-mm2="back"]'); if (back) { e.preventDefault(); showTop(); return; } 20803 const card = e.target.closest('[data-mm2="card"]'); 20804 if (card && !e.metaKey && !e.ctrlKey) { 20805 e.preventDefault(); 20806 const n = data.find(d => d.path === card.dataset.path); 20807 if (n) showDetail(n); 20808 } 20809 const nav = e.target.closest('[data-mm2="nav"]'); 20810 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20811 }); 20812 20813 // Swipe-down gesture for mobile (bottom sheet close) 20814 if (!isDesktop()) { 20815 let startY = 0, currentY = 0, isDragging = false, startTime = 0; 20816 const SWIPE_THRESHOLD = 80; 20817 const VELOCITY_THRESHOLD = 0.3; 20818 20819 panel.addEventListener('touchstart', (e) => { 20820 if (!document.body.classList.contains('mm2-open')) return; 20821 const touch = e.touches[0]; 20822 const scrollableContent = e.target.closest('.mm2-top, .mm2-detail'); 20823 20824 // Prevent drag if content is scrolled 20825 if (scrollableContent && scrollableContent.scrollTop > 5) return; 20826 20827 // Only start drag from top area or handle 20828 const rect = panel.getBoundingClientRect(); 20829 if (touch.clientY - rect.top > 60) return; 20830 20831 startY = touch.clientY; 20832 currentY = startY; 20833 startTime = Date.now(); 20834 isDragging = true; 20835 }, { passive: true }); 20836 20837 panel.addEventListener('touchmove', (e) => { 20838 if (!isDragging) return; 20839 const touch = e.touches[0]; 20840 currentY = touch.clientY; 20841 const deltaY = currentY - startY; 20842 20843 // Only allow downward swipe 20844 if (deltaY > 0) { 20845 const opacity = Math.max(0.4, 1 - (deltaY / 300)); 20846 panel.style.transform = `translateX(0) translateY(${deltaY}px)`; 20847 panel.style.opacity = opacity; 20848 } 20849 }, { passive: true });
20850 20851 const endDrag = () => { 20852 if (!isDragging) return; 20853 const deltaY = currentY - startY; 20854 const deltaTime = Date.now() - startTime; 20855 const velocity = deltaY / deltaTime; 20856 20857 // Close if swiped down enough or fast swipe 20858 if (deltaY > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) { 20859 close(); 20860 } 20861 20862 // Reset position 20863 panel.style.transform = ''; 20864 panel.style.opacity = ''; 20865 isDragging = false; 20866 }; 20867 20868 panel.addEventListener('touchend', endDrag, { passive: true }); 20869 panel.addEventListener('touchcancel', endDrag, { passive: true }); 20870 } 20871 20872 // Fetch data with loading state 20873 if (!isLoading) { 20874 isLoading = true; 20875 showLoading(); 20876 20877 fetch(apiUrl, { headers: { 'Accept': 'application/json' }}) 20878 .then(r => { 20879 if (!r.ok) throw new Error('Network response was not ok'); 20880 return r.json(); 20881 }) 20882 .then(j => { 20883 data = Array.isArray(j.data) ? j.data : []; 20884 if (!data.length) { 20885 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20886 <p>Tidak ada kategori tersedia</p> 20887 </div></div>`; 20888 return; 20889 } 20890 buildTop(); 20891 isLoading = false; 20892 }) 20893 .catch(err => { 20894 console.error('Failed to load categories:', err); 20895 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20896 <p>Gagal memuat kategori. Silakan coba lagi.</p> 20897 </div></div>`; 20898 isLoading = false; 20899 }); 20900 } 20901})(); 20902 20903// Add skeleton animation styles 20904const style = document.createElement('style'); 20905style.textContent = ` 20906.mm2-skeleton { 20907 pointer-events: none; 20908 animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; 20909} 20910.skeleton-box, .skeleton-line { 20911 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 20912 background-size: 200% 100%; 20913 animation: shimmer 1.5s infinite; 20914 border-radius: 8px; 20915} 20916@keyframes pulse { 20917 0%, 100% { opacity: 1; } 20918 50% { opacity: 0.6; } 20919} 20920@keyframes shimmer { 20921 0% { background-position: 200% 0; } 20922 100% { background-position: -200% 0; } 20923} 20924`; 20925document.head.appendChild(style); 20926</script>
20926 20927
20927<script data-inlined="benchmark.js"> 20928/** 20929 * Benchmark Data Loader 20930 * Handles AJAX fetch and display of benchmark data 20931 */ 20932 20933(function() { 20934 'use strict'; 20935 20936 // Wait for DOM ready 20937 if (document.readyState === 'loading') { 20938 document.addEventListener('DOMContentLoaded', initBenchmark); 20939 } else { 20940 initBenchmark(); 20941 } 20942 20943 function initBenchmark() { 20944 const section = document.querySelector('.pd-benchmark-section'); 20945 if (!section) return; 20946 20947 const button = document.getElementById('btn-load-benchmark'); 20948 const content = document.getElementById('benchmark-content'); 20949 const productId = section.dataset.productId; 20950 20951 if (!button || !content || !productId) return; 20952 20953 let isLoading = false; 20954 let dataLoaded = false; 20955 20956 button.addEventListener('click', function() { 20957 if (isLoading || dataLoaded) return; 20958 20959 loadBenchmarkData(productId, button, content); 20960 }); 20961 } 20962 20963 function loadBenchmarkData(productId, button, container) { 20964 // Update button state 20965 button.disabled = true; 20966 button.classList.add('loading'); 20967 const originalText = button.querySelector('.btn-text').textContent; 20968 button.querySelector('.btn-text').textContent = 'Memuat data...'; 20969 20970 // Show loading state 20971 container.style.display = 'block'; 20972 container.innerHTML = ` 20973 <div class="benchmark-loading"> 20974 <div class="benchmark-loading-spinner"></div> 20975 <p>Mengambil data benchmark...</p> 20976 </div> 20977 `; 20978 20979 // Fetch data 20980 fetch('/api/benchmark/fetch.php?product_id=' + productId) 20981 .then(function(response) { 20982 if (!response.ok) { 20983 throw new Error('HTTP error ' + response.status); 20984 } 20985 return response.json(); 20986 }) 20987 .then(function(result) { 20988 if (result.success && result.data) { 20989 renderBenchmarkData(result.data, container); 20990 button.style.display = 'none'; // Hide button after successful load 20991 } else { 20992 throw new Error(result.error || 'Data tidak tersedia'); 20993 } 20994 }) 20995 .catch(function(error) { 20996 console.error('Benchmark fetch error:', error); 20997 container.innerHTML = ` 20998 <div class="benchmark-error"> 20999 <p><strong>â ï¸ Data Benchmark Tidak Tersedia</strong></p> 21000 <p style="font-size: 14px; margin-top: 8px;"> 21001 ${error.message || 'Terjadi kesalahan saat memuat data'} 21002 </p> 21003 <button type="button" onclick="location.reload()" 21004 style="margin-top: 16px; padding: 8px 20px; background: #EAB308; color: #fff; border: none; border-radius: 6px; cursor: pointer;"> 21005 Coba Lagi 21006 </button> 21007 </div> 21008 `; 21009 21010 // Reset button 21011 button.disabled = false; 21012 button.classList.remove('loading'); 21013 button.querySelector('.btn-text').textContent = originalText; 21014 }); 21015 } 21016 21017 function renderBenchmarkData(data, container) { 21018 let html = '<div class="benchmark-data-wrapper">'; 21019 21020 // Chipset info if available 21021 if (data.chipset) { 21022 html += ` 21023 <div style="background: linear-gradient(135deg, #EAB308 0%, #8B6914 100%); color: white; padding: 10px 16px; border-radius: 8px; margin-bottom: 12px;"> 21024 <div style="font-size: 11px; opacity: 0.85; margin-bottom: 2px;">Chipset</div> 21025 <div style="font-size: 15px; font-weight: 600;">${escapeHtml(data.chipset)}</div> 21026 </div> 21027 `; 21028 } 21029 21030 // Main benchmark scores 21031 if (data.antutu || data.geekbench_multi || data.overall_score) { 21032 html += '<div class="benchmark-data-grid">'; 21033 21034 if (data.antutu) { 21035 html += ` 21036 <div class="benchmark-card"> 21037 <div class="benchmark-card-label">ANTUTU</div> 21038 <div class="benchmark-card-value large">${formatNumber(data.antutu)}</div> 21039 ${getScoreBadge(data.antutu, 'antutu')} 21040 </div> 21041 `; 21042 } 21043 21044 if (data.geekbench_multi) { 21045 html += ` 21046 <div class="benchmark-card"> 21047 <div class="benchmark-card-label">GEEKBENCH MULTI</div> 21048 <div class="benchmark-card-value">${formatNumber(data.geekbench_multi)}</div> 21049 ${data.geekbench_single ? '<div class="benchmark-card-subtitle">Single: ' + formatNumber(data.geekbench_single) + '</div>' : ''} 21050 </div> 21051 `; 21052 } 21053 21054 if (data.gaming_score) { 21055 html += ` 21056 <div class="benchmark-card"> 21057 <div class="benchmark-card-label">GAMING</div> 21058 <div class="benchmark-card-value">${data.gaming_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21059 </div> 21060 `; 21061 } 21062 21063 if (data.battery_score) { 21064 html += ` 21065 <div class="benchmark-card"> 21066 <div class="benchmark-card-label">BATTERY</div> 21067 <div class="benchmark-card-value">${data.battery_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21068 </div> 21069 `; 21070 } 21071 21072 if (data.overall_score) { 21073 html += ` 21074 <div class="benchmark-card"> 21075 <div class="benchmark-card-label">OVERALL</div> 21076 <div class="benchmark-card-value">${data.overall_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21077 </div> 21078 `; 21079 } 21080 21081 html += '</div>'; 21082 } 21083 21084 // AI Performance Description 21085 if (data.notes) { 21086 html += ` 21087 <div style="background: #f8f9fa; border-left: 3px solid #EAB308; padding: 10px 14px; border-radius: 6px; margin-top: 12px;"> 21088 <div style="font-size: 11px; font-weight: 600; color: #EAB308; margin-bottom: 6px;">ð¡ Analisis Performa</div> 21089 <div style="font-size: 13px; color: #4a5568; line-height: 1.5;">${escapeHtml(data.notes)}</div> 21090 </div> 21091 `; 21092 } 21093 21094 // Attribution 21095 html += ` 21096 <div class="benchmark-attribution"> 21097 <p style="font-size: 11px; margin: 8px 0 0 0; color: #888;">Data benchmark dianalisis menggunakan AI dari spesifikasi produk</p> 21098 ${data.fetched_at ? '<p style="font-size: 10px; margin: 4px 0 0 0; color: #aaa;">Terakhir diperbarui: ' + formatDate(data.fetched_at) + '</p>' : ''} 21099 </div> 21100 `; 21101 21102 html += '</div>'; 21103 21104 container.innerHTML = html; 21105 } 21106 21107 function formatNumber(num) { 21108 if (!num) return '-'; 21109 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 21110 } 21111 21112 function escapeHtml(text) { 21113 if (!text) return ''; 21114 const div = document.createElement('div'); 21115 div.textContent = text; 21116 return div.innerHTML; 21117 } 21118 21119 function formatDate(dateStr) { 21120 try { 21121 const date = new Date(dateStr); 21122 const options = { year: 'numeric', month: 'long', day: 'numeric' }; 21123 return date.toLocaleDateString('id-ID', options); 21124 } catch (e) { 21125 return dateStr; 21126 } 21127 } 21128 21129 function getScoreBadge(score, type) { 21130 let category = ''; 21131 let label = ''; 21132 21133 if (type === 'antutu') { 21134 if (score >= 500000) { 21135 category = 'excellent'; 21136 label = 'Flagship'; 21137 } else if (score >= 350000) { 21138 category = 'good'; 21139 label = 'High-End'; 21140 } else if (score >= 200000) { 21141 category = 'average'; 21142 label = 'Mid-Range'; 21143 } else { 21144 category = 'average'; 21145 label = 'Entry-Level'; 21146 } 21147 } 21148 21149 if (label) { 21150 return '<div class="benchmark-score-badge ' + category + '">' + label + '</div>'; 21151 } 21152 21153 return ''; 21154 } 21155})(); 21156 21157</script>
21157 21158
21158<script data-inlined="product-compare.js"> 21159(function() { 21160 'use strict'; 21161 21162 // State management 21163 let compareList = []; 21164 let isAnalyzing = false; 21165 let currentAnalysis = null; 21166 21167 // DOM elements 21168 const compareBtn = document.getElementById('compareBtn'); 21169 const compareSidebar = document.getElementById('compareSidebar'); 21170 const compareOverlay = document.getElementById('compareOverlay'); 21171 const compareClose = document.getElementById('compareClose'); 21172 const compareModal = document.getElementById('aiCompareModal'); 21173 const compareModalClose = document.getElementById('aiCompareModalClose'); 21174 21175 // Initialize 21176 document.addEventListener('DOMContentLoaded', async function() { 21177 await initializeCompare(); 21178 setupEventListeners(); 21179 }); 21180 21181 /** 21182 * Initialize compare functionality 21183 */ 21184 async function initializeCompare() { 21185 // Load compare list first before setting up buttons 21186 await loadCompareList(); 21187 21188 // Setup product detail compare button if exists 21189 const productCompareBtn = document.getElementById('pd-compare-btn'); 21190 21191 if (productCompareBtn) { 21192 setupProductCompareButton(productCompareBtn); 21193 } 21194 21195 // Check for existing compare list and update UI 21196 if (compareList.length > 0) { 21197 // Only update badge and buttons, no sticky bar 21198 } 21199 } 21200 21201 /** 21202 * Setup event listeners 21203 */ 21204 function setupEventListeners() { 21205 // Navigation compare button 21206 if (compareBtn) { 21207 compareBtn.addEventListener('click', toggleCompareSidebar); 21208 } 21209 21210 // Sidebar controls 21211 if (compareClose) { 21212 compareClose.addEventListener('click', closeCompareSidebar); 21213 } 21214 21215 if (compareOverlay) { 21216 compareOverlay.addEventListener('click', closeCompareSidebar); 21217 } 21218 21219 // Sidebar actions 21220 const clearAllBtn = document.getElementById('compareClearAll'); 21221 if (clearAllBtn) { 21222 clearAllBtn.addEventListener('click', clearAllProducts); 21223 } 21224 21225 const analyzeBtn = document.getElementById('compareAnalyze'); 21226 if (analyzeBtn) { 21227 analyzeBtn.addEventListener('click', startAIAnalysis); 21228 } 21229 21230 // Modal controls 21231 if (compareModalClose) { 21232 compareModalClose.addEventListener('click', closeModal); 21233 } 21234 21235 const modalBackdrop = compareModal?.querySelector('.modal-backdrop'); 21236 if (modalBackdrop) { 21237 modalBackdrop.addEventListener('click', closeModal); 21238 } 21239 21240 // Modal actions 21241 const retryBtn = document.getElementById('aiRetry'); 21242 if (retryBtn) { 21243 retryBtn.addEventListener('click', startAIAnalysis); 21244 } 21245 21246 // Removed share and add to cart buttons - replaced with AI disclaimer 21247 } 21248 21249 /** 21250 * Setup product detail compare button 21251 */ 21252 function setupProductCompareButton(button) { 21253 const productId = parseInt(button.dataset.productId); 21254 if (!productId) return; 21255 21256 // Check if product is already in compare list 21257 updateProductCompareButtonState(button, productId); 21258 21259 button.addEventListener('click', async function() { 21260 // Get current variant ID (if any) 21261 const variantIdElement = document.getElementById('pd-variant-id'); 21262 const variantId = variantIdElement ? parseInt(variantIdElement.value) || 0 : 0; 21263 21264 // Update button's variant data for current selection 21265 button.dataset.variantId = variantId.toString(); 21266 21267 await toggleProductInCompare(productId, button, variantId); 21268 }); 21269 } 21270 21271 /** 21272 * Toggle product in compare list 21273 */ 21274 async function toggleProductInCompare(productId, button, variantId = 0) { 21275 // Check by product ID (variants of same product treated as same item) 21276 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21277 21278 try { 21279 if (isInCompare) { 21280 await removeFromCompare(productId); 21281 showToast('Produk dihapus dari perbandingan', 'success'); 21282 } else { 21283 await addToCompare(productId, variantId); 21284 showToast('Produk ditambahkan ke perbandingan', 'success'); 21285 21286 // Trigger fly-to-compare animation 21287 triggerFlyToCompareAnimation(button); 21288 } 21289 21290 updateProductCompareButtonState(button, productId); 21291 updateCompareBadge(); 21292 updateAllCompareButtons(); 21293 21294 } catch (error) { 21295 showToast(error.message || 'Terjadi kesalahan', 'error'); 21296 } 21297 } 21298 21299 /** 21300 * Update product compare button state 21301 */ 21302 function updateProductCompareButtonState(button, productId) { 21303 // Ensure both IDs are compared as integers 21304 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21305 button.classList.toggle('added', isInCompare); 21306 21307 // Handle visibility of text elements 21308 const compareText = button.querySelector('.compare-text'); 21309 const compareRemove = button.querySelector('.compare-remove'); 21310 const compareNote = document.getElementById('compare-note'); 21311 21312 if (compareText) { 21313 compareText.style.display = isInCompare ? 'none' : 'inline'; 21314 } 21315 21316 if (compareRemove) { 21317 if (isInCompare) { 21318 compareRemove.removeAttribute('hidden'); 21319 compareRemove.style.display = 'flex'; 21320 } else { 21321 compareRemove.setAttribute('hidden', ''); 21322 compareRemove.style.display = 'none'; 21323 } 21324 } 21325 21326 // Update info note 21327 if (compareNote) { 21328 if (isInCompare) { 21329 const currentCount = compareList.length; 21330 compareNote.textContent = `Produk sudah ada di daftar perbandingan (${currentCount}/5). Klik tombol untuk menghapus.`; 21331 } else { 21332 compareNote.textContent = 'Bandingkan hingga 5 produk dengan bantuan AI'; 21333 } 21334 } 21335 21336 // Inline compare preview removed - keeping it simple 21337 } 21338 21339 /** 21340 * Add product to compare list 21341 */ 21342 async function addToCompare(productId, variantId = 0) { 21343 const requestData = { product_id: productId }; 21344 if (variantId > 0) { 21345 requestData.variant_id = variantId; 21346 } 21347 21348 const response = await fetch('/api/compare/add.php', { 21349 method: 'POST', 21350 headers: { 21351 'Content-Type': 'application/json', 21352 }, 21353 body: JSON.stringify(requestData), 21354 credentials: 'same-origin' 21355 });
21356 21357 const result = await response.json(); 21358 21359 if (!result.success) { 21360 throw new Error(result.error || 'Gagal menambahkan produk'); 21361 } 21362 21363 // Send Google Analytics event for add to compare 21364 if (typeof gtag === 'function') { 21365 gtag('event', 'add_to_compare', { 21366 event_category: 'Product Compare', 21367 event_label: 'Product Added to Compare', 21368 product_id: productId, 21369 variant_id: variantId || 0, 21370 custom_map: { 21371 custom_parameter_1: 'compare_add_product' 21372 } 21373 }); 21374 } 21375 21376 await loadCompareList(); 21377 return result; 21378 } 21379 21380 /** 21381 * Remove product from compare list 21382 */ 21383 async function removeFromCompare(productId) { 21384 const response = await fetch('/api/compare/remove.php', { 21385 method: 'POST', 21386 headers: { 21387 'Content-Type': 'application/json', 21388 }, 21389 body: JSON.stringify({ product_id: productId }), 21390 credentials: 'same-origin' 21391 }); 21392 21393 const result = await response.json(); 21394 21395 if (!result.success) { 21396 throw new Error(result.error || 'Gagal menghapus produk'); 21397 } 21398 21399 // Send Google Analytics event for remove from compare 21400 if (typeof gtag === 'function') { 21401 gtag('event', 'remove_from_compare', { 21402 event_category: 'Product Compare', 21403 event_label: 'Product Removed from Compare', 21404 product_id: productId, 21405 custom_map: { 21406 custom_parameter_1: 'compare_remove_product' 21407 } 21408 }); 21409 } 21410 21411 await loadCompareList(); 21412 return result; 21413 } 21414 21415 /** 21416 * Clear all products from compare list 21417 */ 21418 async function clearAllProducts() { 21419 if (compareList.length === 0) return; 21420 21421 // Show confirmation modal 21422 const confirmed = await showConfirmModal({ 21423 title: 'Hapus Semua Produk', 21424 message: 'Apakah Anda yakin ingin menghapus semua produk dari daftar perbandingan?', 21425 confirmText: 'Hapus Semua', 21426 cancelText: 'Batal', 21427 type: 'danger' 21428 }); 21429 21430 if (!confirmed) return; 21431 21432 try { 21433 const response = await fetch('/api/compare/clear.php', { 21434 method: 'POST', 21435 credentials: 'same-origin' 21436 }); 21437 21438 const result = await response.json(); 21439 21440 if (!result.success) { 21441 throw new Error(result.error || 'Gagal menghapus produk'); 21442 } 21443 21444 // Send Google Analytics event for clear all products 21445 if (typeof gtag === 'function') { 21446 gtag('event', 'compare_clear_all', { 21447 event_category: 'Product Compare', 21448 event_label: 'Clear All Products', 21449 products_count: compareList.length, 21450 custom_map: { 21451 custom_parameter_1: 'compare_clear_action' 21452 } 21453 }); 21454 } 21455 21456 await loadCompareList(); 21457 showToast('Semua produk dihapus dari perbandingan', 'success'); 21458 21459 } catch (error) { 21460 showToast(error.message || 'Terjadi kesalahan', 'error'); 21461 } 21462 } 21463 21464 /** 21465 * Load compare list from server 21466 */ 21467 async function loadCompareList() { 21468 try { 21469 const response = await fetch('/api/compare/get.php', { 21470 credentials: 'same-origin' 21471 }); 21472 21473 const result = await response.json(); 21474 21475 if (result.success) { 21476 compareList = result.products || []; 21477 updateCompareSidebar(); 21478 updateCompareBadge(); 21479 updateAllCompareButtons(); 21480 } 21481 21482 } catch (error) { 21483 // Silently handle error - compare functionality will work with empty list 21484 } 21485 } 21486 21487 /** 21488 * Update compare sidebar content 21489 */ 21490 function updateCompareSidebar() { 21491 const emptyState = document.getElementById('compareEmpty'); 21492 const listContainer = document.getElementById('compareList'); 21493 const actionsContainer = document.getElementById('compareActions'); 21494 const countElement = document.getElementById('compareCountSidebar'); 21495 21496 if (!emptyState || !listContainer || !actionsContainer || !countElement) return; 21497 21498 // Update count 21499 countElement.textContent = `(${compareList.length})`; 21500 21501 if (compareList.length === 0) { 21502 emptyState.style.display = 'block'; 21503 listContainer.style.display = 'none'; 21504 actionsContainer.style.display = 'none'; 21505 } else { 21506 emptyState.style.display = 'none'; 21507 listContainer.style.display = 'block'; 21508 actionsContainer.style.display = 'flex'; 21509 21510 // Render product list 21511 listContainer.innerHTML = compareList.map(product => { 21512 // Display variant name if available 21513 let displayName = product.name; 21514 if (product.combination_name) { 21515 displayName += ` (${product.combination_name})`; 21516 } 21517 21518 // Use variant price if available 21519 let displayPrice = product.price; 21520 if (product.variant_price !== undefined && product.variant_price !== null) { 21521 displayPrice = product.variant_price; 21522 } 21523 21524 return ` 21525 <div class="compare-item" data-product-id="${product.id}"> 21526 <div class="compare-item-image"> 21527 <img src="${product.image_url}" alt="${escapeHtml(displayName)}" loading="lazy"> 21528 </div> 21529 <div class="compare-item-info"> 21530 <div class="compare-item-name">${escapeHtml(displayName)}</div> 21531 <div class="compare-item-price">${formatPrice(displayPrice)}</div> 21532 </div> 21533 <button class="compare-item-remove" data-product-id="${product.id}" aria-label="Hapus">
21534 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 21535 <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"/> 21536 </svg> 21537 </button> 21538 </div> 21539 `; 21540 }).join(''); 21541 21542 // Setup event listeners for remove buttons 21543 setupRemoveButtons(); 21544 21545 // Update analyze button state 21546 const analyzeBtn = document.getElementById('compareAnalyze'); 21547 if (analyzeBtn) { 21548 analyzeBtn.disabled = compareList.length < 2; 21549 } 21550 } 21551 } 21552 21553 /** 21554 * Setup remove buttons event listeners 21555 */ 21556 function setupRemoveButtons() { 21557 const removeButtons = document.querySelectorAll('.compare-item-remove'); 21558 removeButtons.forEach(button => { 21559 button.addEventListener('click', async (e) => { 21560 e.preventDefault(); 21561 const productId = parseInt(button.dataset.productId); 21562 if (productId) { 21563 await removeProduct(productId); 21564 } 21565 }); 21566 }); 21567 } 21568 21569 /** 21570 * Update all compare buttons on the page 21571 */ 21572 function updateAllCompareButtons() { 21573 // Update product detail compare button 21574 const productBtn = document.getElementById('pd-compare-btn'); 21575 if (productBtn) { 21576 const productId = parseInt(productBtn.dataset.productId); 21577 if (productId) { 21578 updateProductCompareButtonState(productBtn, productId); 21579 } 21580 } 21581 21582 // Update any other compare buttons on the page 21583 const allCompareButtons = document.querySelectorAll('[data-product-id][class*="compare"]'); 21584 allCompareButtons.forEach(button => { 21585 const productId = parseInt(button.dataset.productId); 21586 if (productId && button.id !== 'pd-compare-btn') { 21587 updateProductCompareButtonState(button, productId); 21588 } 21589 }); 21590 } 21591 21592 /** 21593 * Update compare badge 21594 */ 21595 function updateCompareBadge() { 21596 const badge = document.getElementById('compareCount'); 21597 if (!badge) return; 21598 21599 const oldCount = parseInt(badge.textContent) || 0; 21600 const newCount = compareList.length; 21601 21602 badge.textContent = newCount; 21603 badge.classList.toggle('active', newCount > 0); 21604 21605 // Animate badge if count increased 21606 if (newCount > oldCount) { 21607 animateCompareBadge(); 21608 highlightCompareButton(); 21609 } 21610 21611 // Sticky bar removed - keeping only fly-to-navbar animation 21612 } 21613 21614 /** 21615 * Animate compare badge when new product added 21616 */ 21617 function animateCompareBadge() { 21618 const badge = document.getElementById('compareCount'); 21619 if (!badge) return; 21620 21621 badge.style.transform = 'scale(1.3)'; 21622 badge.style.transition = 'transform 0.3s ease'; 21623 21624 setTimeout(() => { 21625 badge.style.transform = 'scale(1)'; 21626 }, 300); 21627 } 21628 21629 /** 21630 * Highlight compare button temporarily 21631 */ 21632 function highlightCompareButton() { 21633 const compareBtn = document.getElementById('compareBtn'); 21634 if (!compareBtn) return; 21635 21636 compareBtn.classList.add('pulse-highlight'); 21637 21638 setTimeout(() => { 21639 compareBtn.classList.remove('pulse-highlight'); 21640 }, 3000); 21641 } 21642 21643 /** 21644 * Trigger fly-to-compare animation 21645 */ 21646 function triggerFlyToCompareAnimation(sourceButton) { 21647 if (!sourceButton) { 21648 return; 21649 } 21650 21651 // Check if mobile view (bottom nav) or desktop (top nav) 21652 const isMobile = window.innerWidth <= 768; 21653 21654 // Get source position 21655 const sourceRect = sourceButton.getBoundingClientRect(); 21656 21657 // Calculate target position 21658 let targetTop, targetLeft; 21659 21660 if (isMobile) { 21661 // Mobile: fly to fixed bottom position (bottom nav area) 21662 // Use fixed coordinates regardless of bottom nav visibility 21663 targetTop = window.innerHeight - 80; // Just above bottom nav area 21664 targetLeft = window.innerWidth / 2 - 30; // Center horizontally 21665 } else { 21666 // Desktop: fly to top navbar compare button 21667 const targetElement = document.getElementById('compareBtn'); 21668 if (!targetElement) { 21669 return; 21670 } 21671 const targetRect = targetElement.getBoundingClientRect(); 21672 targetTop = targetRect.top + 10; 21673 targetLeft = targetRect.left + 10; 21674 } 21675 21676 // Get product image for animation - try multiple selectors 21677 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"]'); 21678 21679 // Create animated element 21680 const flyingElement = document.createElement('div'); 21681 flyingElement.className = 'flying-product'; 21682 21683 if (productImage) { 21684 flyingElement.innerHTML = `<img src="${productImage.src}" alt="Flying product">`; 21685 flyingElement.querySelector('img').style.cssText = ` 21686 width: 100%; 21687 height: 100%; 21688 object-fit: cover; 21689 border-radius: 8px; 21690 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21691 `; 21692 } else { 21693 // Fallback: colored div with icon 21694 flyingElement.innerHTML = `<div class="flying-fallback">ð¦</div>`; 21695 flyingElement.querySelector('.flying-fallback').style.cssText = ` 21696 width: 100%; 21697 height: 100%; 21698 background: #EAB308; 21699 border-radius: 8px; 21700 display: flex; 21701 align-items: center; 21702 justify-content: center; 21703 font-size: 24px; 21704 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21705 `; 21706 } 21707 21708 flyingElement.style.cssText = ` 21709 position: fixed; 21710 top: ${sourceRect.top}px; 21711 left: ${sourceRect.left}px; 21712 width: 60px; 21713 height: 60px; 21714 z-index: 9999; 21715 pointer-events: none; 21716 opacity: 1;
21717 transform: scale(1); 21718 `; 21719 21720 document.body.appendChild(flyingElement); 21721 21722 // Use Web Animations API for more reliable animation 21723 flyingElement.animate([ 21724 // Keyframe 1: Initial state 21725 { 21726 top: `${sourceRect.top}px`, 21727 left: `${sourceRect.left}px`, 21728 transform: 'scale(1)', 21729 opacity: 1 21730 }, 21731 // Keyframe 2: Final state 21732 { 21733 top: `${targetTop}px`, 21734 left: `${targetLeft}px`, 21735 transform: 'scale(0.3)', 21736 opacity: 0 21737 } 21738 ], { 21739 duration: 800, 21740 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 21741 fill: 'forwards' 21742 }); 21743 21744 // Clean up after animation completes 21745 setTimeout(() => { 21746 if (flyingElement.parentNode) { 21747 flyingElement.parentNode.removeChild(flyingElement); 21748 } 21749 }, 850); 21750 } 21751 21752 // Sticky compare bar functions removed - using only fly-to-navbar animation 21753 21754 /** 21755 * Toggle compare sidebar 21756 */ 21757 function toggleCompareSidebar() { 21758 if (!compareSidebar) return; 21759 21760 const isOpen = compareSidebar.classList.contains('open'); 21761 21762 if (isOpen) { 21763 closeCompareSidebar(); 21764 } else { 21765 openCompareSidebar(); 21766 } 21767 } 21768 21769 /** 21770 * Open compare sidebar 21771 */ 21772 function openCompareSidebar() { 21773 if (!compareSidebar || !compareOverlay) return; 21774 21775 compareSidebar.classList.add('open'); 21776 compareOverlay.classList.add('active'); 21777 document.body.style.overflow = 'hidden'; 21778 21779 // Send Google Analytics event for compare sidebar open 21780 if (typeof gtag === 'function') { 21781 gtag('event', 'compare_sidebar_open', { 21782 event_category: 'Product Compare', 21783 event_label: 'Compare Sidebar Opened', 21784 products_count: compareList.length, 21785 custom_map: { 21786 custom_parameter_1: 'compare_sidebar_view' 21787 } 21788 }); 21789 } 21790 } 21791 21792 /** 21793 * Close compare sidebar 21794 */ 21795 function closeCompareSidebar() { 21796 if (!compareSidebar || !compareOverlay) return; 21797 21798 compareSidebar.classList.remove('open'); 21799 compareOverlay.classList.remove('active'); 21800 document.body.style.overflow = ''; 21801 } 21802 21803 /** 21804 * Start AI analysis 21805 */ 21806 async function startAIAnalysis() { 21807 if (compareList.length < 2) { 21808 showToast('Minimal 2 produk diperlukan untuk analisis AI', 'warning'); 21809 return; 21810 } 21811 21812 if (isAnalyzing) return; 21813 21814 // Send Google Analytics event for AI Analysis start 21815 if (typeof gtag === 'function') { 21816 gtag('event', 'ai_analysis_start', { 21817 event_category: 'Product Compare', 21818 event_label: 'AI Analysis Button Click', 21819 products_count: compareList.length, 21820 product_ids: compareList.map(p => p.id).join(','), 21821 custom_map: { 21822 custom_parameter_1: 'compare_ai_analysis' 21823 } 21824 }); 21825 } 21826 21827 // Check if products are in similar categories 21828 const mismatchInfo = await checkProductCategoryMismatch(); 21829 if (mismatchInfo.shouldWarn) { 21830 const categoryList = Array.from(mismatchInfo.categories).join(', '); 21831 const confirmed = await showConfirmModal({ 21832 title: 'Produk Berbeda Kategori', 21833 message: `Produk yang Anda pilih berasal dari kategori yang berbeda: ${categoryList}. AI tetap bisa menganalisis, tapi perbandingan mungkin kurang relevan. Lanjutkan analisis?`, 21834 confirmText: 'Ya, Analisis Tetap', 21835 cancelText: 'Batal', 21836 type: 'warning' 21837 }); 21838 21839 if (!confirmed) return; 21840 } 21841 21842 openModal(); 21843 showLoadingState(); 21844 isAnalyzing = true; 21845 21846 try { 21847 const response = await fetch('/api/compare/ai-analysis.php', { 21848 method: 'POST', 21849 credentials: 'same-origin' 21850 }); 21851 21852 const result = await response.json(); 21853 21854 if (!result.success) { 21855 throw new Error(result.error || 'Analisis AI gagal'); 21856 } 21857 21858 currentAnalysis = result; 21859 showAnalysisResults(result); 21860 21861 // Send Google Analytics event for successful AI Analysis 21862 if (typeof gtag === 'function') { 21863 gtag('event', 'ai_analysis_success', { 21864 event_category: 'Product Compare', 21865 event_label: 'AI Analysis Completed', 21866 products_count: compareList.length, 21867 analysis_type: 'product_comparison', 21868 custom_map: { 21869 custom_parameter_1: 'compare_ai_success' 21870 } 21871 }); 21872 } 21873 21874 } catch (error) { 21875 showErrorState(error.message); 21876 21877 // Send Google Analytics event for failed AI Analysis 21878 if (typeof gtag === 'function') { 21879 gtag('event', 'ai_analysis_error', { 21880 event_category: 'Product Compare', 21881 event_label: 'AI Analysis Failed', 21882 products_count: compareList.length, 21883 error_message: error.message, 21884 custom_map: { 21885 custom_parameter_1: 'compare_ai_error' 21886 } 21887 }); 21888 } 21889 } finally { 21890 isAnalyzing = false; 21891 } 21892 } 21893 21894 /** 21895 * Show modal 21896 */ 21897 function openModal() { 21898 if (!compareModal) return; 21899 compareModal.classList.add('open'); 21900 document.body.style.overflow = 'hidden'; 21901 } 21902 21903 /** 21904 * Close modal 21905 */ 21906 function closeModal() { 21907 if (!compareModal) return; 21908 compareModal.classList.remove('open'); 21909 document.body.style.overflow = ''; 21910 } 21911 21912 /** 21913 * Show loading state 21914 */ 21915 function showLoadingState() { 21916 const loading = document.getElementById('aiLoading'); 21917 const error = document.getElementById('aiError'); 21918 const results = document.getElementById('aiResults'); 21919 21920 if (loading) loading.style.display = 'block'; 21921 if (error) error.hidden = true; 21922 if (results) results.hidden = true; 21923 } 21924 21925 /** 21926 * Show error state 21927 */ 21928 function showErrorState(message) { 21929 const loading = document.getElementById('aiLoading'); 21930 const error = document.getElementById('aiError'); 21931 const results = document.getElementById('aiResults'); 21932 const errorMessage = document.getElementById('aiErrorMessage'); 21933 21934 if (loading) loading.style.display = 'none'; 21935 if (error) error.hidden = false; 21936 if (results) results.hidden = true; 21937 if (errorMessage) errorMessage.textContent = message; 21938 } 21939 21940 /** 21941 * Show analysis results 21942 */ 21943 function showAnalysisResults(analysis) { 21944 const loading = document.getElementById('aiLoading'); 21945 const error = document.getElementById('aiError'); 21946 const results = document.getElementById('aiResults'); 21947 21948 if (loading) loading.style.display = 'none'; 21949 if (error) error.hidden = true; 21950 if (results) results.hidden = false; 21951 21952 const data = analysis.data; 21953 21954 // Update summary 21955 const summaryElement = document.getElementById('aiSummary'); 21956 if (summaryElement) summaryElement.textContent = data.summary || ''; 21957 21958 const winnerProduct = document.getElementById('winnerProduct'); 21959 const winnerReason = document.getElementById('winnerReason'); 21960 if (winnerProduct && winnerReason && data.winner_overall) { 21961 const winner = compareList.find(p => parseInt(p.id) === parseInt(data.winner_overall)); 21962 winnerProduct.textContent = winner ? winner.name : 'Tidak diketa
21962hui'; 21963 winnerReason.textContent = data.winner_reason || ''; 21964 } 21965 21966 // Update comparison table 21967 updateComparisonTable(data.comparison_table); 21968 21969 // Update detailed analysis 21970 updateDetailedAnalysis(data.detailed_analysis); 21971 21972 // Update recommendations 21973 updateRecommendations(data.recommendations); 21974 21975 // Update buying tips 21976 updateBuyingTips(data.buying_tips); 21977 } 21978 21979 /** 21980 * Update comparison table 21981 */ 21982 function updateComparisonTable(comparisonData) { 21983 const container = document.getElementById('comparisonTable'); 21984 if (!container || !comparisonData?.categories) return; 21985 21986 // First, create product header row with names 21987 const productHeaderHtml = ` 21988 <div class="comparison-category"> 21989 <div class="category-header">Produk</div> 21990 <div class="category-products"> 21991 ${compareList.map(product => ` 21992 <div class="product-score"> 21993 <div class="product-name-header"> 21994 <strong>${escapeHtml(product.name)}</strong> 21995 </div> 21996 <div class="product-price"> 21997 ${formatPrice(product.price)} 21998 </div> 21999 </div> 22000 `).join('')} 22001 </div> 22002 </div> 22003 `; 22004 22005 // Then create comparison categories 22006 const categoriesHtml = comparisonData.categories.map(category => ` 22007 <div class="comparison-category"> 22008 <div class="category-header">${escapeHtml(category.name)}</div> 22009 <div class="category-products"> 22010 ${compareList.map(product => { 22011 const productData = category.products?.[product.id]; 22012 return ` 22013 <div class="product-score"> 22014 <div class="score-value"> 22015 <span class="score-stars">${'â '.repeat(productData?.score || 0)}${'â'.repeat(5 - (productData?.score || 0))}</span> 22016 <span>${productData?.score || 0}/5</span> 22017 </div> 22018 <div class="score-note">${escapeHtml(productData?.note || '')}</div> 22019 </div> 22020 `; 22021 }).join('')} 22022 </div> 22023 </div> 22024 `).join(''); 22025 22026 container.innerHTML = productHeaderHtml + categoriesHtml; 22027 } 22028 22029 /** 22030 * Update detailed analysis 22031 */ 22032 function updateDetailedAnalysis(analysisData) { 22033 const container = document.getElementById('detailedAnalysis'); 22034 if (!container || !analysisData) return; 22035 22036 container.innerHTML = compareList.map(product => { 22037 const productAnalysis = analysisData[product.id]; 22038 if (!productAnalysis) return ''; 22039 22040 // Use product name from API response if available, fallback to local name 22041 const productName = productAnalysis.product_name || product.name; 22042 22043 return ` 22044 <div class="product-analysis"> 22045 <h5>${escapeHtml(productName)}</h5> 22046 22047 ${productAnalysis.strengths ? ` 22048 <div class="analysis-section"> 22049 <h6 class="strengths">â Kelebihan</h6> 22050 <ul> 22051 ${productAnalysis.strengths.map(strength => 22052 `<li>${escapeHtml(strength)}</li>` 22053 ).join('')} 22054 </ul> 22055 </div> 22056 ` : ''} 22057 22058 ${productAnalysis.weaknesses ? ` 22059 <div class="analysis-section"> 22060 <h6 class="weaknesses">â Kekurangan</h6> 22061 <ul> 22062 ${productAnalysis.weaknesses.map(weakness => 22063 `<li>${escapeHtml(weakness)}</li>` 22064 ).join('')} 22065 </ul> 22066 </div> 22067 ` : ''} 22068 22069 ${productAnalysis.best_for ? ` 22070 <div class="analysis-section"> 22071 <h6 class="best-for">ð¯ Cocok Untuk</h6> 22072 <p class="best-for-text">${escapeHtml(productAnalysis.best_for)}</p> 22073 </div> 22074 ` : ''} 22075 </div> 22076 `; 22077 }).filter(Boolean).join(''); 22078 } 22079 22080 /** 22081 * Update recommendations 22082 */ 22083 function updateRecommendations(recommendations) { 22084 if (!recommendations) return; 22085 22086 const budgetEl = document.getElementById('budgetRecommendation'); 22087 const performanceEl = document.getElementById('performanceRecommendation'); 22088 const balancedEl = document.getElementById('balancedRecommendation'); 22089 22090 if (budgetEl && recommendations.budget_conscious) { 22091 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.budget_conscious)); 22092 budgetEl.textContent = product ? product.name : 'Tidak tersedia'; 22093 } 22094 22095 if (performanceEl && recommendations.performance_focused) { 22096 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.performance_focused)); 22097 performanceEl.textContent = product ? product.name : 'Tidak tersedia'; 22098 } 22099 22100 if (balancedEl && recommendations.balanced) { 22101 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.balanced)); 22102 balancedEl.textContent = product ? product.name : 'Tidak tersedia'; 22103 } 22104 } 22105 22106 /** 22107 * Check if products are from different categories 22108 */ 22109 async function checkProductCategoryMismatch() { 22110 if (compareList.length < 2) { 22111 return { shouldWarn: false, categories: new Set() }; 22112 } 22113 22114 // Get the root categories from each product 22115 const rootCategories = new Set(); 22116 const productCategories = []; 22117 22118 for (const product of compareList) { 22119 // Extract root category from the product's category_path if available 22120 const rootCategory = await getProductRootCategory(product); 22121 if (rootCategory) { 22122 rootCategories.add(rootCategory); 22123 productCategories.push({ 22124 product: product.name, 22125 category: rootCategory 22126 }); 22127 } 22128 } 22129 22130 // If we have more than 1 different root categories, show warning 22131 const shouldWarn = rootCategories.size > 1 && compareList.length >= 2; 22132 22133 return { 22134 shouldWarn: shouldWarn, 22135 categories: rootCategories, 22136 productCategories: productCategories 22137 }; 22138 } 22139 22140 /** 22141 * Get product root category from category_path or fallback to name detection 22142 */ 22143 async function getProductRootCategory(product) { 22144 // First try to get from category_path if available 22145 if (product.category_path) { 22146 const rootCategory = product.category_path.split('/')[0]; 22147 return rootCategory; 22148 } 22149 22150 // If no category_path, try to fetch from product detail or use name detection 22151 const detectedCategory = detectProductCategory(product.name); 22152 return detectedCategory; 22153 } 22154 22155 /** 22156 * Detect product category from name (fallback method) 22157 */ 22158 function detectProductCategory(productName) { 22159 const name = productName.toLowerCase(); 22160 22161 // Map keywords to actual root categories from the API structure 22162 const categoryMappings = { 22163 // Komputer & Laptop 22164 'Komputer & Laptop': ['laptop', 'notebook', 'macbook', 'thinkpad', 'gaming laptop', 'desktop', 'pc', 'mini pc', 'all-in-one', 'server'], 22165 22166 // Printer & Scanner 22167 'Printer & Scanner': ['printer', 'epson printer', 'canon printer', 'hp printer', 'brother printer', 'scanner', 'laser printer', 'inkjet', 'dot matrix'], 22168 22169 // Gadget 22170 'Gadget': ['smartphone', 'iphone', 'samsung phone', 'xiaomi', 'oppo', 'vivo', 'realme', 'tablet', 'ipad', 'android tablet'], 22171 22172 // Periferal
22173 'Periferal': ['monitor', 'lcd monitor', 'led monitor', 'gaming monitor', 'keyboard', 'mouse', 'gaming keyboard', 'mechanical keyboard', 'webcam'], 22174 22175 // Audio 22176 'Audio': ['headset', 'headphone', 'earphone', 'gaming headset', 'speaker', 'mikrofon', 'sound card'], 22177 22178 // Jaringan 22179 'Jaringan': ['router', 'wifi', 'access point', 'switch', 'modem', 'network'], 22180 22181 // Penyimpanan 22182 'Penyimpanan': ['ssd', 'hdd', 'nvme', 'solid state', 'hard disk', 'flash drive', 'usb drive', 'memory card'], 22183 22184 // Komponen PC 22185 'Komponen PC': ['ram', 'memory', 'ddr4', 'ddr5', 'processor', 'cpu', 'intel', 'amd', 'motherboard', 'graphics card', 'gpu', 'psu', 'power supply', 'casing'], 22186 22187 // UPS & Power 22188 'UPS & Power': ['ups', 'uninterruptible', 'stabilizer', 'power supply'], 22189 22190 // Proyektor 22191 'Proyektor': ['proyektor', 'projector', 'layar proyektor'], 22192 22193 // Security System 22194 'Security System': ['cctv', 'security camera', 'surveillance'], 22195 22196 // Aksesoris 22197 'Aksesoris': ['charger laptop', 'adaptor', 'converter', 'dock', 'hub', 'kabel', 'cable', 'power bank', 'tas laptop'], 22198 22199 // Software 22200 'Software': ['software', 'lisensi', 'license', 'antivirus'], 22201 22202 // Perlengkapan Kantor 22203 'Perlengkapan Kantor': ['fingerprint', 'finger print', 'atk', 'alat tulis'] 22204 }; 22205 22206 // Check which root category matches 22207 for (const [rootCategory, keywords] of Object.entries(categoryMappings)) { 22208 for (const keyword of keywords) { 22209 if (name.includes(keyword)) { 22210 return rootCategory; 22211 } 22212 } 22213 } 22214 22215 return null; 22216 } 22217 22218 /** 22219 * Update buying tips 22220 */ 22221 function updateBuyingTips(tips) { 22222 const container = document.getElementById('buyingTips'); 22223 if (!container || !Array.isArray(tips)) return; 22224 22225 container.innerHTML = tips.map(tip => 22226 `<li>${escapeHtml(tip)}</li>` 22227 ).join(''); 22228 } 22229 22230 // Removed share and cart functions - replaced with AI disclaimer in UI 22231 22232 /** 22233 * Remove product from compare (public method) 22234 */ 22235 async function removeProduct(productId) { 22236 try { 22237 await removeFromCompare(productId); 22238 showToast('Produk dihapus dari perbandingan', 'success'); 22239 22240 // Update all compare buttons 22241 updateAllCompareButtons(); 22242 22243 } catch (error) { 22244 showToast(error.message || 'Terjadi kesalahan', 'error'); 22245 } 22246 } 22247 22248 /** 22249 * Utility functions 22250 */ 22251 function escapeHtml(text) { 22252 const div = document.createElement('div'); 22253 div.textContent = text; 22254 return div.innerHTML; 22255 } 22256 22257 function formatPrice(price) { 22258 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 22259 } 22260 22261 /** 22262 * Show confirmation modal using the existing modal.css design 22263 */ 22264 function showConfirmModal(options = {}) { 22265 return new Promise((resolve) => { 22266 const { 22267 title = 'Konfirmasi', 22268 message = 'Apakah Anda yakin?', 22269 confirmText = 'Ya', 22270 cancelText = 'Tidak', 22271 type = 'warning' 22272 } = options; 22273 22274 // Create modal HTML 22275 const modalHTML = ` 22276 <div class="modal-overlay" id="confirmModal"> 22277 <div class="modal-dialog"> 22278 <div class="modal-header"> 22279 <h3 class="modal-title"> 22280 <div class="modal-icon ${type}"> 22281 ${getModalIcon(type)} 22282 </div> 22283 ${escapeHtml(title)} 22284 </h3> 22285 </div> 22286 <div class="modal-body"> 22287 <p class="modal-message">${escapeHtml(message)}</p> 22288 <div class="modal-actions"> 22289 <button type="button" class="modal-btn modal-btn-secondary" id="confirmCancel"> 22290 ${escapeHtml(cancelText)} 22291 </button> 22292 <button type="button" class="modal-btn modal-btn-${type === 'danger' ? 'danger' : 'primary'}" id="confirmOk"> 22293 ${escapeHtml(confirmText)} 22294 </button> 22295 </div> 22296 </div> 22297 </div> 22298 </div> 22299 `; 22300 22301 // Add to DOM 22302 document.body.insertAdjacentHTML('beforeend', modalHTML); 22303 const modal = document.getElementById('confirmModal'); 22304 const cancelBtn = document.getElementById('confirmCancel'); 22305 const okBtn = document.getElementById('confirmOk'); 22306 22307 // Show modal 22308 requestAnimationFrame(() => { 22309 modal.classList.add('show'); 22310 }); 22311 22312 // Handle events 22313 function cleanup() { 22314 modal.remove(); 22315 } 22316 22317 function handleCancel() { 22318 modal.classList.remove('show'); 22319 setTimeout(() => { 22320 cleanup(); 22321 resolve(false); 22322 }, 200); 22323 } 22324 22325 function handleConfirm() { 22326 modal.classList.remove('show'); 22327 setTimeout(() => { 22328 cleanup(); 22329 resolve(true); 22330 }, 200); 22331 } 22332 22333 cancelBtn.addEventListener('click', handleCancel); 22334 okBtn.addEventListener('click', handleConfirm); 22335 22336 // Close on overlay click 22337 modal.addEventListener('click', (e) => { 22338 if (e.target === modal) { 22339 handleCancel(); 22340 } 22341 }); 22342 22343 // Close on escape 22344 document.addEventListener('keydown', function escapeHandler(e) { 22345 if (e.key === 'Escape') { 22346 document.removeEventListener('keydown', escapeHandler); 22347 handleCancel(); 22348 } 22349 }); 22350 22351 // Focus on confirm button 22352 setTimeout(() => okBtn.focus(), 100); 22353 });
22354 } 22355 22356 /** 22357 * Get icon for modal type 22358 */ 22359 function getModalIcon(type) { 22360 const icons = { 22361 danger: 'ð¨ ', 22362 warning: 'â ï¸', 22363 success: 'â ', 22364 info: 'â¹ï¸' 22365 }; 22366 return icons[type] || icons.info; 22367 } 22368 22369 22370 function showToast(message, type = 'info') { 22371 // Integration with existing toast system or create simple one 22372 22373 // Simple alert fallback - replace with proper toast system 22374 if (type === 'error') { 22375 alert('Error: ' + message); 22376 } 22377 } 22378 22379 // Inline compare preview functions removed - keeping it simple and clean 22380 22381 /** 22382 * Get current product ID from page 22383 */ 22384 function getCurrentProductId() { 22385 const button = document.getElementById('pd-compare-btn'); 22386 return button ? parseInt(button.dataset.productId) : null; 22387 } 22388 22389 // hideStickyCompareBar function removed - no longer using sticky bar 22390 22391 // Public API 22392 window.productCompare = { 22393 removeProduct: removeProduct, 22394 addToCompare: addToCompare, 22395 loadCompareList: loadCompareList, 22396 getCompareList: function() { return compareList; }, 22397 openSidebar: openCompareSidebar, 22398 startAnalysis: startAIAnalysis, 22399 // Helper function to add from external sources with variant support 22400 addProductWithVariant: async function(productId, variantId = 0) { 22401 return await addToCompare(productId, variantId); 22402 } 22403 }; 22404 22405})(); 22406 22407 22408 22409</script>
vendor: 2 bytes, line 22409
22409
22410<script data-inlined="product-card-actions.js"> 22411/** 22412 * Product Card Quick Actions 22413 * Bottom sheet menu for quick add to cart & compare 22414 * Integrates with existing product-compare.js and cart.js 22415 */ 22416 22417(function() { 22418 'use strict'; 22419 22420 let activeSheet = null; 22421 let activeProductData = null; 22422 22423 // DOM elements (will be created dynamically) 22424 let overlay = null; 22425 let bottomSheet = null; 22426 22427 /** 22428 * Initialize on DOM ready 22429 */ 22430 document.addEventListener('DOMContentLoaded', function() { 22431 createBottomSheet(); 22432 attachEventListeners(); 22433 }); 22434 22435 /** 22436 * Create bottom sheet HTML structure 22437 */ 22438 function createBottomSheet() { 22439 // Create overlay 22440 overlay = document.createElement('div'); 22441 overlay.className = 'pca-overlay'; 22442 overlay.addEventListener('click', closeBottomSheet); 22443 22444 // Create bottom sheet 22445 bottomSheet = document.createElement('div'); 22446 bottomSheet.className = 'pca-bottom-sheet'; 22447 bottomSheet.innerHTML = ` 22448 <div class="pca-sheet-handle"></div> 22449 <div class="pca-sheet-product" id="pca-product-preview"> 22450 <!-- Product info will be inserted here --> 22451 </div> 22452 <div class="pca-sheet-actions"> 22453 <button type="button" class="pca-action-item cart" id="pca-add-cart"> 22454 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22455 <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"/> 22456 </svg> 22457 <div class="pca-action-content"> 22458 <div class="pca-action-label">Tambah ke Keranjang</div> 22459 <div class="pca-action-desc">Lanjut belanja produk lain</div> 22460 </div> 22461 </button> 22462 22463 <button type="button" class="pca-action-item compare" id="pca-add-compare"> 22464 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22465 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 22466 </svg> 22467 <div class="pca-action-content"> 22468 <div class="pca-action-label">Bandingkan Produk</div> 22469 <div class="pca-action-desc">Analisis dengan AI</div> 22470 </div> 22471 <span class="pca-action-badge compare" id="pca-compare-count" style="display:none;">0</span> 22472 </button> 22473 </div> 22474 `; 22475 22476 // Append to body 22477 document.body.appendChild(overlay); 22478 document.body.appendChild(bottomSheet); 22479 22480 // Setup action buttons 22481 document.getElementById('pca-add-cart').addEventListener('click', handleAddToCart); 22482 document.getElementById('pca-add-compare').addEventListener('click', handleAddToCompare); 22483 22484 // Prevent clicks inside bottom sheet from closing it 22485 bottomSheet.addEventListener('click', function(e) { 22486 e.stopPropagation(); 22487 }); 22488 } 22489 22490 /** 22491 * Attach event listeners to all 3-dot buttons 22492 */ 22493 function attachEventListeners() { 22494 // Use event delegation for dynamically loaded product cards 22495 document.addEventListener('click', function(e) { 22496 const btn = e.target.closest('.prod-card-actions-btn'); 22497 if (btn) { 22498 e.preventDefault(); 22499 e.stopPropagation(); 22500 handleActionsButtonClick(btn); 22501 } 22502 }); 22503 22504 // Handle ESC key 22505 document.addEventListener('keydown', function(e) { 22506 if (e.key === 'Escape' && activeSheet) { 22507 closeBottomSheet(); 22508 } 22509 }); 22510 } 22511 22512 /** 22513 * Handle 3-dot button click 22514 */ 22515 function handleActionsButtonClick(button) { 22516 // Get product ID directly from button 22517 const productId = parseInt(button.dataset.productId) || 0; 22518 if (!productId) { 22519 console.error('Product ID not found on button:', button); 22520 showToast('Data produk tidak ditemukan', 'error'); 22521 return; 22522 } 22523 22524 // Find parent card for additional data 22525 const card = button.closest('.prod-card, .pa-card'); 22526 if (!card) { 22527 console.error('Product card not found'); 22528 showToast('Data produk tidak ditemukan', 'error'); 22529 return; 22530 } 22531 22532 // Extract product data from card 22533 const productData = extractProductData(card, productId); 22534 if (!productData) { 22535 showToast('Data produk tidak ditemukan', 'error'); 22536 return; 22537 } 22538 22539 activeProductData = productData; 22540 openBottomSheet(productData); 22541 } 22542 22543 /** 22544 * Extract product data from card element 22545 */ 22546 function extractProductData(card, productId) { 22547 try { 22548 // Product ID already passed from button 22549 if (!productId) { 22550 console.error('Product ID not provided'); 22551 return null; 22552 } 22553 // Product ID already passed from button 22554 if (!productId) { 22555 console.error('Product ID not provided'); 22556 return null; 22557 } 22558 22559 const id = productId; 22560 22561 // Get product name 22562 const nameEl = card.querySelector('.prod-name, .title'); 22563 const name = nameEl ? nameEl.textContent.trim() : 'Produk'; 22564 22565 // Get product image 22566 const imgEl = card.querySelector('.thumb img, img'); 22567 const image = imgEl ? imgEl.src : '/assets//img/placeholder.png'; 22568 22569 // Get product price and detect variants 22570 const priceEl = card.querySelector('.prod-price-final, .final, .prod-price'); 22571 let price = 0; 22572 let hasVariants = false; 22573 let priceDisplay = ''; 22574 22575 if (priceEl) { 22576 // Get text content and clean it 22577 let priceText = priceEl.textContent.trim(); 22578 priceDisplay = priceText; 22579 22580 // Check if it's a range (indicates product has variants) 22581 if (priceText.includes('-')) { 22582 hasVariants = true; 22583 // Take the first price for calculation 22584 priceText = priceText.split('-')[0].trim(); 22585 } 22586 22587 // Remove all non-numeric characters 22588 priceText = priceText.replace(/[^0-9]/g, ''); 22589 price = parseInt(priceText) || 0; 22590 } 22591 22592 // Get stock status 22593 const stockDot = card.querySelector('.stock-dot'); 22594 let stockStatus = 'unknown'; 22595 let stockLabel = 'Cek stok'; 22596 22597 if (stockDot) { 22598 if (stockDot.classList.contains('ready')) { 22599 stockStatus = 'in'; 22600 stockLabel = 'Stok tersedia'; 22601 } else if (stockDot.classList.contains('low')) { 22602 stockStatus = 'low'; 22603 stockLabel = 'Stok terbatas'; 22604 } else if (stockDot.classList.contains('out')) { 22605 stockStatus = 'out'; 22606 stockLabel = 'Stok habis'; 22607 } 22608 } 22609 22610 // Get category path (if available) 22611 const categoryPath = card.dataset.category || ''; 22612 22613 // Get URL 22614 let url = '#'; 22615 if (card.tagName === 'A') { 22616 url = card.href; 22617 } else { 22618 const link = card.querySelector('a[href*="/product/"]'); 22619 if (link) url = link.href; 22620 } 22621 22622 return { 22623 id: id, 22624 name: name, 22625 image: image, 22626 price: price, 22627 priceDisplay: priceDisplay, 22628 hasVariants: hasVariants, 22629 stockStatus: stockStatus, 22630 stockLabel: stockLabel, 22631 categoryPath: categoryPath, 22632 url: url 22633 }; 22634 } catch (error) { 22635 console.error('Error extracting product data:', error); 22636 return null; 22637 } 22638 } 22639 22640 /** 22641 * Open bottom sheet 22642 */ 22643 function openBottomSheet(productData) { 22644 if (!overlay || !bottomSheet) return; 22645 22646 // Update product preview 22647 updateProductPreview(productData); 22648 22649 // Update cart button based on variant status
22650 updateCartButtonForVariant(productData); 22651 22652 // Update compare button state 22653 updateCompareButtonState(productData.id); 22654 22655 // Show overlay and sheet 22656 overlay.classList.add('active'); 22657 bottomSheet.classList.add('active'); 22658 document.body.style.overflow = 'hidden'; 22659 22660 activeSheet = true; 22661 22662 // Google Analytics 22663 if (typeof gtag === 'function') { 22664 gtag('event', 'quick_actions_open', { 22665 event_category: 'Product Card', 22666 event_label: 'Quick Actions Menu Opened', 22667 product_id: productData.id, 22668 product_name: productData.name 22669 }); 22670 } 22671 } 22672 22673 /** 22674 * Close bottom sheet 22675 */ 22676 function closeBottomSheet() { 22677 if (!overlay || !bottomSheet) return; 22678 22679 overlay.classList.remove('active'); 22680 bottomSheet.classList.remove('active'); 22681 document.body.style.overflow = ''; 22682 22683 activeSheet = false; 22684 activeProductData = null; 22685 } 22686 22687 /** 22688 * Update product preview in bottom sheet 22689 */ 22690 function updateProductPreview(productData) { 22691 const preview = document.getElementById('pca-product-preview'); 22692 if (!preview) return; 22693 22694 // Use full price display (shows range for variants) 22695 const priceDisplay = productData.priceDisplay || formatPrice(productData.price); 22696 22697 preview.innerHTML = ` 22698 <img src="${escapeHtml(productData.image)}" 22699 alt="${escapeHtml(productData.name)}" 22700 class="pca-product-image" 22701 loading="lazy"> 22702 <div class="pca-product-info"> 22703 <h4 class="pca-product-name">${escapeHtml(productData.name)}</h4> 22704 <p class="pca-product-price">${escapeHtml(priceDisplay)}</p> 22705 <p class="pca-product-stock ${productData.stockStatus}">${productData.stockLabel}</p> 22706 </div> 22707 `; 22708 } 22709 22710 /** 22711 * Update cart button for variant products 22712 */ 22713 function updateCartButtonForVariant(productData) { 22714 const cartBtn = document.getElementById('pca-add-cart'); 22715 if (!cartBtn) return; 22716 22717 if (productData.hasVariants) { 22718 // Product has variants - change to "View Detail" 22719 cartBtn.dataset.hasVariants = 'true'; 22720 cartBtn.dataset.productUrl = productData.url; 22721 22722 // Update icon to eye/external 22723 const iconSvg = cartBtn.querySelector('svg'); 22724 if (iconSvg) { 22725 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"/>'; 22726 } 22727 22728 // Update text 22729 const label = cartBtn.querySelector('.pca-action-label'); 22730 const desc = cartBtn.querySelector('.pca-action-desc'); 22731 if (label) label.textContent = 'Lihat Detail Produk'; 22732 if (desc) desc.textContent = 'Pilih varian terlebih dahulu'; 22733 22734 } else { 22735 // Single product - normal cart button 22736 cartBtn.dataset.hasVariants = 'false'; 22737 22738 // Restore cart icon 22739 const iconSvg = cartBtn.querySelector('svg'); 22740 if (iconSvg) { 22741 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"/>'; 22742 } 22743 22744 // Restore text 22745 const label = cartBtn.querySelector('.pca-action-label'); 22746 const desc = cartBtn.querySelector('.pca-action-desc'); 22747 if (label) label.textContent = 'Tambah ke Keranjang'; 22748 if (desc) desc.textContent = 'Lanjut belanja produk lain'; 22749 } 22750 } 22751 22752 /** 22753 * Update compare button state 22754 */ 22755 function updateCompareButtonState(productId) { 22756 const compareBtn = document.getElementById('pca-add-compare'); 22757 const compareBadge = document.getElementById('pca-compare-count'); 22758 22759 if (!compareBtn) return; 22760 22761 // Check if product is in compare list (using global compareList if available) 22762 let isInCompare = false;
22763 let compareCount = 0; 22764 22765 // Try to get compare list from product-compare.js via window.productCompare 22766 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.getCompareList === 'function') { 22767 const compareList = window.productCompare.getCompareList(); 22768 compareCount = compareList.length; 22769 isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 22770 } else { 22771 // Fallback: check badge count 22772 const mainBadge = document.getElementById('compareCount'); 22773 if (mainBadge) { 22774 compareCount = parseInt(mainBadge.textContent) || 0; 22775 } 22776 } 22777 22778 // Update button state 22779 if (isInCompare) { 22780 compareBtn.classList.add('active'); 22781 compareBtn.querySelector('.pca-action-label').textContent = 'Hapus dari Perbandingan'; 22782 compareBtn.querySelector('.pca-action-desc').textContent = 'Sudah ada di daftar'; 22783 } else { 22784 compareBtn.classList.remove('active'); 22785 compareBtn.querySelector('.pca-action-label').textContent = 'Bandingkan Produk'; 22786 compareBtn.querySelector('.pca-action-desc').textContent = 'Analisis dengan AI'; 22787 } 22788 22789 // Update badge 22790 if (compareBadge && compareCount > 0) { 22791 compareBadge.textContent = compareCount; 22792 compareBadge.style.display = 'inline-flex'; 22793 } else if (compareBadge) { 22794 compareBadge.style.display = 'none'; 22795 } 22796 } 22797 22798 /** 22799 * Handle add to cart 22800 */ 22801 async function handleAddToCart() { 22802 if (!activeProductData) return; 22803 22804 const button = document.getElementById('pca-add-cart'); 22805 if (!button) return; 22806 22807 // If product has variants, redirect to detail page 22808 if (activeProductData.hasVariants) { 22809 window.location.href = activeProductData.url; 22810 return; 22811 } 22812 22813 // Check stock 22814 if (activeProductData.stockStatus === 'out') { 22815 showToast('Produk sedang habis', 'warning'); 22816 return; 22817 } 22818 22819 // Show loading state 22820 button.classList.add('loading'); 22821 button.disabled = true; 22822 22823 // CSRF Token Helper 22824 function getCsrfToken() { 22825 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 22826 } 22827 22828 try { 22829 // Call cart API with proper format 22830 const response = await fetch('/api/cart/add.php', { 22831 method: 'POST', 22832 headers: { 22833 'Content-Type': 'application/json', 22834 'X-CSRF-Token': getCsrfToken() 22835 }, 22836 body: JSON.stringify({ 22837 csrf_token: getCsrfToken(), 22838 product_id: activeProductData.id, 22839 qty: 1, // Use 'qty' not 'quantity' 22840 variant_id: 0, // Default variant 22841 meta: { 22842 name: activeProductData.name, 22843 image_url: activeProductData.image 22844 } 22845 }), 22846 credentials: 'same-origin' 22847 }); 22848 22849 const result = await response.json(); 22850 22851 // Check both response status and result.ok 22852 if (!response.ok || !result.ok) { 22853 throw new Error(result.error || result.message || 'Gagal menambahkan ke keranjang'); 22854 } 22855 22856 // Success animation 22857 button.classList.remove('loading'); 22858 button.classList.add('success'); 22859 22860 // Update cart badge 22861 updateCartBadge(); 22862 22863 // PlazaAnalytics tracking 22864 if (window.PlazaAnalytics) { 22865 const _slug = (activeProductData.url || '').replace(/.*\/product\//, ''); 22866 window.PlazaAnalytics.track('add_to_cart', { slug: _slug }); 22867 } 22868 22869 // Show success toast 22870 showToast('Produk ditambahkan ke keranjang', 'success'); 22871 22872 // Google Analytics 22873 if (typeof gtag === 'function') { 22874 gtag('event', 'add_to_cart', { 22875 event_category: 'Ecommerce', 22876 event_label: 'Quick Add to Cart', 22877 items: [{ 22878 item_id: activeProductData.id, 22879 item_name: activeProductData.name, 22880 price: activeProductData.price, 22881 quantity: 1 22882 }] 22883 }); 22884 } 22885 22886 // Close sheet after short delay 22887 setTimeout(() => { 22888 closeBottomSheet(); 22889 button.classList.remove('success'); 22890 button.disabled = false; 22891 }, 1000); 22892 22893 } catch (error) { 22894 console.error('Add to cart error:', error); 22895 button.classList.remove('loading'); 22896 button.disabled = false; 22897 showToast(error.message || 'Terjadi kesalahan', 'error'); 22898 } 22899 } 22900 22901 /** 22902 * Handle add to compare 22903 */ 22904 async function handleAddToCompare() { 22905 if (!activeProductData) return; 22906 22907 const button = document.getElementById('pca-add-compare'); 22908 if (!button) return; 22909 22910 // Check if already in compare 22911 const isActive = button.classList.contains('active'); 22912 22913 // Check 5-product limit before adding 22914 if (!isActive) { 22915 const mainBadge = document.getElementById('compareCount'); 22916 const currentCount = mainBadge ? parseInt(mainBadge.textContent) || 0 : 0; 22917 22918 if (currentCount >= 5) { 22919 showToast('Maksimal 5 produk untuk perbandingan', 'warning'); 22920 return; 22921 } 22922 } 22923 22924 // Show loading state 22925 button.classList.add('loading'); 22926 button.disabled = true; 22927 22928 try { 22929 if (isActive) { 22930 // Remove from compare 22931 await removeFromCompare(activeProductData.id); 22932 showToast('Produk dihapus dari perbandingan', 'success'); 22933 } else { 22934 // Add to compare 22935 await addToCompare(activeProductData.id); 22936 showToast('Produk ditambahkan ke perbandingan', 'success'); 22937 22938 // Trigger fly animation 22939 triggerFlyToCompareAnimation(); 22940 } 22941 22942 // Success animation 22943 button.classList.remove('loading'); 22944 button.classList.add('success'); 22945 22946 // Update compare button state 22947 updateCompareButtonState(activeProductData.id); 22948 22949 // Close sheet after short delay 22950 setTimeout(() => { 22951 closeBottomSheet(); 22952 button.classList.remove('success'); 22953 button.disabled = false; 22954 }, 800); 22955 22956 } catch (error) { 22957 console.error('Compare error:', error); 22958 button.classList.remove('loading'); 22959 button.disabled = false; 22960 showToast(error.message || 'Terjadi kesalahan', 'error'); 22961 } 22962 } 22963 22964 /** 22965 * Add to compare (calls existing API) 22966 */ 22967 async function addToCompare(productId) { 22968 const response = await fetch('/api/compare/add.php', { 22969 method: 'POST', 22970 headers: { 22971 'Content-Type': 'application/json', 22972 }, 22973 body: JSON.stringify({ 22974 product_id: productId, 22975 variant_id: 0 22976 }), 22977 credentials: 'same-origin' 22978 });
22979 22980 const result = await response.json(); 22981 22982 if (!result.success) { 22983 throw new Error(result.error || 'Gagal menambahkan produk'); 22984 } 22985 22986 // Trigger compare list reload if function exists 22987 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 22988 await window.productCompare.loadCompareList(); 22989 } 22990 22991 return result; 22992 } 22993 22994 /** 22995 * Remove from compare (calls existing API) 22996 */ 22997 async function removeFromCompare(productId) { 22998 const response = await fetch('/api/compare/remove.php', { 22999 method: 'POST', 23000 headers: { 23001 'Content-Type': 'application/json', 23002 }, 23003 body: JSON.stringify({ 23004 product_id: productId 23005 }), 23006 credentials: 'same-origin' 23007 }); 23008 23009 const result = await response.json(); 23010 23011 if (!result.success) { 23012 throw new Error(result.error || 'Gagal menghapus produk'); 23013 } 23014 23015 // Trigger compare list reload if function exists 23016 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23017 await window.productCompare.loadCompareList(); 23018 } 23019 23020 return result; 23021 } 23022 23023 /** 23024 * Update cart badge 23025 */ 23026 function updateCartBadge() { 23027 // Try to call global cart update function if exists 23028 if (typeof window.updateCartBadge === 'function') { 23029 window.updateCartBadge(); 23030 } else { 23031 // Fallback: increment badge manually 23032 const badge = document.getElementById('cartCount'); 23033 if (badge) { 23034 const currentCount = parseInt(badge.textContent) || 0; 23035 badge.textContent = currentCount + 1; 23036 badge.classList.add('active'); 23037 } 23038 } 23039 } 23040 23041 /** 23042 * Trigger fly to compare animation 23043 */ 23044 function triggerFlyToCompareAnimation() { 23045 const compareBtn = document.getElementById('compareBtn'); 23046 if (!compareBtn || !activeProductData) return; 23047 23048 // Create flying element 23049 const flyingElement = document.createElement('div'); 23050 flyingElement.className = 'flying-product'; 23051 flyingElement.style.cssText = ` 23052 position: fixed; 23053 width: 60px; 23054 height: 60px; 23055 z-index: 99999; 23056 pointer-events: none; 23057 `; 23058 23059 flyingElement.innerHTML = ` 23060 <img src="${activeProductData.image}" 23061 style="width:100%;height:100%;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);"> 23062 `; 23063 23064 // Position at bottom sheet 23065 const sheetRect = bottomSheet.getBoundingClientRect(); 23066 const targetRect = compareBtn.getBoundingClientRect(); 23067 23068 flyingElement.style.top = `${sheetRect.top + 20}px`; 23069 flyingElement.style.left = `${sheetRect.left + 20}px`; 23070 23071 document.body.appendChild(flyingElement); 23072 23073 // Animate to target 23074 requestAnimationFrame(() => { 23075 flyingElement.style.transition = 'all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; 23076 flyingElement.style.top = `${targetRect.top + 10}px`; 23077 flyingElement.style.left = `${targetRect.left + 10}px`; 23078 flyingElement.style.transform = 'scale(0.3)'; 23079 flyingElement.style.opacity = '0'; 23080 }); 23081 23082 // Clean up 23083 setTimeout(() => { 23084 if (flyingElement.parentNode) { 23085 flyingElement.parentNode.removeChild(flyingElement); 23086 } 23087 }, 800); 23088 } 23089 23090 /** 23091 * Format price as Rupiah 23092 */ 23093 function formatPrice(price) { 23094 if (!price || price === 0) return 'Rp 0'; 23095 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 23096 } 23097 23098 /** 23099 * Escape HTML 23100 */ 23101 function escapeHtml(text) { 23102 const div = document.createElement('div'); 23103 div.textContent = text; 23104 return div.innerHTML; 23105 } 23106 23107 /** 23108 * Show toast notification (uses global showToast if available) 23109 */ 23110 function showToast(message, type = 'info') { 23111 if (typeof window.showToast === 'function') { 23112 window.showToast({ 23113 title: type === 'success' ? 'Berhasil' : type === 'error' ? 'Gagal' : 'Info', 23114 message: message, 23115 type: type, 23116 duration: 3000 23117 }); 23118 } else { 23119 // Fallback: console log 23120 console.log(`[${type.toUpperCase()}] ${message}`); 23121 } 23122 } 23123 23124 /** 23125 * Expose functions for external access 23126 */ 23127 window.ProductCardActions = {
23128 openSheet: openBottomSheet, 23129 closeSheet: closeBottomSheet, 23130 updateCompareState: updateCompareButtonState 23131 }; 23132 23133})(); 23134 23135</script>
23135 23136
23136<script data-inlined="product-reviews.js"> 23137/** 23138 * Product Reviews - Frontend JavaScript 23139 * Handles review display, submission, and helpful voting 23140 */ 23141 23142const ProductReviews = (function() { 23143 'use strict'; 23144 23145 let currentPage = 1; 23146 let currentSort = 'helpful'; 23147 let productId = null; 23148 let isLoading = false; 23149 23150 /** 23151 * Public API: Initialize reviews for a product 23152 */ 23153 function init(pid) { 23154 productId = pid; 23155 23156 if (!productId) { 23157 console.warn('Product ID not provided to ProductReviews.init()'); 23158 return; 23159 } 23160 23161 initializeReviewTab(); 23162 loadReviews(); 23163 } 23164 23165 /** 23166 * Initialize review tab functionality 23167 */ 23168 function initializeReviewTab() { 23169 // Tab switching 23170 const tabButtons = document.querySelectorAll('.tab-btn'); 23171 tabButtons.forEach(btn => { 23172 btn.addEventListener('click', function() { 23173 const tabName = this.dataset.tab; 23174 switchTab(tabName); 23175 }); 23176 }); 23177 23178 // Sort dropdown 23179 const sortSelect = document.getElementById('review-sort'); 23180 if (sortSelect) { 23181 sortSelect.addEventListener('change', function() { 23182 currentSort = this.value; 23183 currentPage = 1; 23184 loadReviews(); 23185 }); 23186 } 23187 23188 // Load more button
23189 const loadMoreBtn = document.getElementById('load-more-reviews'); 23190 if (loadMoreBtn) { 23191 loadMoreBtn.addEventListener('click', function() { 23192 currentPage++; 23193 loadReviews(true); // Append mode 23194 }); 23195 } 23196 23197 // Write review button 23198 const writeReviewBtn = document.getElementById('write-review-btn'); 23199 if (writeReviewBtn) { 23200 writeReviewBtn.addEventListener('click', showReviewModal); 23201 } 23202 } 23203 23204 /** 23205 * Switch between product tabs 23206 */ 23207 function switchTab(tabName) { 23208 // Update button states 23209 document.querySelectorAll('.tab-btn').forEach(btn => { 23210 btn.classList.toggle('active', btn.dataset.tab === tabName); 23211 }); 23212 23213 // Update content visibility 23214 document.querySelectorAll('.tab-content').forEach(content => { 23215 content.style.display = content.id === `${tabName}-tab` ? 'block' : 'none'; 23216 }); 23217 } 23218 23219 /** 23220 * Load reviews from API 23221 */ 23222 async function loadReviews(append = false) { 23223 if (isLoading) return; 23224 isLoading = true; 23225 23226 const container = document.getElementById('reviews-list');
23227 const loadMoreBtn = document.getElementById('load-more-reviews'); 23228 23229 // Check if review section exists (product might not have reviews section) 23230 if (!container) { 23231 console.log('Review section not found - product may not have reviews yet'); 23232 isLoading = false; 23233 return; 23234 } 23235 23236 try { 23237 if (!append) { 23238 container.innerHTML = '<div class="review-loading">Memuat ulasan...</div>'; 23239 } else { 23240 loadMoreBtn.disabled = true; 23241 loadMoreBtn.textContent = 'Memuat...'; 23242 } 23243 23244 const response = await fetch(`/api/reviews/list.php?product_id=${productId}&sort=${currentSort}&page=${currentPage}&limit=10`); 23245 const data = await response.json(); 23246 23247 if (!data.success) { 23248 throw new Error(data.message || 'Gagal memuat ulasan'); 23249 } 23250 23251 // Update statistics (only on initial load) 23252 if (!append && data.stats) { 23253 updateReviewStats(data.stats); 23254 } 23255 23256 // Render reviews 23257 if (!append) { 23258 container.innerHTML = ''; 23259 } 23260 23261 const reviews = data.reviews || []; 23262 23263 if (reviews.length === 0 && !append) { 23264 container.innerHTML = '<div class="no-reviews">Belum ada ulasan untuk produk ini. Jadilah yang pertama!</div>'; 23265 } else { 23266 reviews.forEach(review => { 23267 container.appendChild(createReviewElement(review)); 23268 }); 23269 } 23270 23271 // Update load more button 23272 if (loadMoreBtn) { 23273 if (data.pagination.has_more) { 23274 loadMoreBtn.style.display = 'block'; 23275 loadMoreBtn.disabled = false; 23276 loadMoreBtn.textContent = 'Muat Lebih Banyak'; 23277 } else { 23278 loadMoreBtn.style.display = 'none'; 23279 } 23280 } 23281 23282 } catch (error) { 23283 console.error('Load reviews error:', error); 23284 container.innerHTML = '<div class="review-error">Gagal memuat ulasan. Silakan coba lagi.</div>'; 23285 } finally { 23286 isLoading = false; 23287 } 23288 } 23289 23290 /** 23291 * Update review statistics display 23292 */ 23293 function updateReviewStats(stats) { 23294 const avgRatingEl = document.getElementById('avg-rating'); 23295 const totalReviewsEl = document.getElementById('total-reviews'); 23296 const ratingBarsContainer = document.getElementById('rating-bars'); 23297 23298 if (avgRatingEl) { 23299 avgRatingEl.textContent = stats.average_rating.toFixed(1); 23300 } 23301 23302 if (totalReviewsEl) { 23303 totalReviewsEl.textContent = `${stats.total_reviews} ulasan`; 23304 } 23305 23306 // Update rating breakdown bars 23307 if (ratingBarsContainer) { 23308 ratingBarsContainer.innerHTML = ''; 23309 stats.rating_breakdown.forEach(item => { 23310 const bar = document.createElement('div'); 23311 bar.className = 'rating-bar'; 23312 bar.innerHTML = ` 23313 <span class="rating-label">${item.rating}â </span> 23314 <div class="bar-container"> 23315 <div class="bar-fill" style="width: ${item.percentage}%"></div> 23316 </div> 23317 <span class="rating-count">${item.count}</span> 23318 `; 23319 ratingBarsContainer.appendChild(bar); 23320 }); 23321 } 23322 23323 // Update tab count 23324 const reviewTab = document.querySelector('[data-tab="reviews"]'); 23325 if (reviewTab && stats.total_reviews > 0) { 23326 reviewTab.textContent = `Ulasan (${stats.total_reviews})`; 23327 } 23328 } 23329 23330 /** 23331 * Create review element 23332 */ 23333 function createReviewElement(review) { 23334 const div = document.createElement('div'); 23335 div.className = 'review-item'; 23336 div.dataset.reviewId = review.ID; 23337 23338 // Build stars 23339 const stars = 'â '.repeat(review.RATING) + 'â'.repeat(5 - review.RATING); 23340 23341 // Build images if available 23342 let imagesHtml = ''; 23343 if (review.images && review.images.length > 0) { 23344 imagesHtml = '<div class="review-images">'; 23345 review.images.forEach(img => { 23346 imagesHtml += `<img src="${escapeHtml(img)}" alt="Review image" class="review-img" loading="lazy">`; 23347 }); 23348 imagesHtml += '</div>'; 23349 } 23350 23351 // Verified purchase badge 23352 const verifiedBadge = review.IS_VERIFIED_PURCHASE === 'Y' 23353 ? '<span class="verified-badge">â Pembelian Terverifikasi</span>' 23354 : ''; 23355 23356 // Use DISPLAY_NAME if available (for anonymous reviews), otherwise use FULL_NAME 23357 const displayName = review.DISPLAY_NAME || review.FULL_NAME || 'Anonymous'; 23358 const anonymousIndicator = review.IS_ANONYMOUS === 'Y' 23359 ? '<span class="anonymous-badge" title="Nama pengguna disamark
23359an untuk privasi">ð</span>' 23360 : ''; 23361 23362 div.innerHTML = ` 23363 <div class="review-header"> 23364 <div class="review-author"> 23365 <strong>${escapeHtml(displayName)}</strong> 23366 ${anonymousIndicator} 23367 ${verifiedBadge} 23368 </div> 23369 <div class="review-rating"> 23370 <span class="stars">${stars}</span> 23371 </div> 23372 </div> 23373 <div class="review-date">${review.created_ago}</div> 23374 <div class="review-text">${escapeHtml(review.REVIEW_TEXT).replace(/\n/g, '<br>')}</div> 23375 ${imagesHtml} 23376 <div class="review-actions"> 23377 <button class="btn-helpful" data-review-id="${review.ID}"> 23378 <span class="helpful-icon">ð</span> 23379 <span class="helpful-text">Membantu (${review.HELPFUL_COUNT || 0})</span> 23380 </button> 23381 </div> 23382 `; 23383 23384 // Add helpful button listener 23385 const helpfulBtn = div.querySelector('.btn-helpful'); 23386 helpfulBtn.addEventListener('click', () => markAsHelpful(review.ID)); 23387 23388 return div; 23389 } 23390 23391 /** 23392 * Get CSRF token from meta tag 23393 */ 23394 function getCsrfToken() { 23395 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23396 return metaTag ? metaTag.getAttribute('content') : ''; 23397 } 23398 23399 /** 23400 * Mark review as helpful 23401 */ 23402 async function markAsHelpful(reviewId) { 23403 // Check if logged in 23404 const isLoggedIn = document.body.classList.contains('logged-in') || 23405 document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 23406 23407 if (!isLoggedIn) { 23408 showModal({ 23409 title: 'Login Diperlukan', 23410 message: 'Silakan login terlebih dahulu untuk memberikan vote pada ulasan ini.', 23411 icon: 'info', 23412 buttons: [ 23413 { text: 'Batal', className: 'modal-btn-secondary' }, 23414 { text: 'Login', className: 'modal-btn-primary', callback: () => window.location.href = '/login' } 23415 ] 23416 }); 23417 return; 23418 } 23419 23420 // Get current button element 23421 const reviewItem = document.querySelector(`[data-review-id="${reviewId}"]`); 23422 const helpfulBtn = reviewItem?.querySelector('.btn-helpful'); 23423 23424 if (!helpfulBtn) return; 23425 23426 // Disable button during request 23427 helpfulBtn.disabled = true; 23428 const originalText = helpfulBtn.querySelector('.helpful-text').textContent; 23429 23430 try { 23431 const response = await fetch('/api/reviews/helpful.php', { 23432 method: 'POST', 23433 headers: { 23434 'Content-Type': 'application/json', 23435 'X-CSRF-Token': getCsrfToken() 23436 }, 23437 credentials: 'same-origin', 23438 body: JSON.stringify({ 23439 csrf_token: getCsrfToken(), 23440 review_id: reviewId 23441 }) 23442 }); 23443 23444 const data = await response.json(); 23445 23446 if (data.success) { 23447 // Update button count immediately based on action 23448 const currentCount = parseInt(originalText.match(/\d+/)?.[0] || 0); 23449 const newCount = data.action === 'added' ? currentCount + 1 : currentCount - 1; 23450 helpfulBtn.querySelector('.helpful-text').textContent = `Membantu (${newCount})`; 23451 23452 // Show success message 23453 showToast(data.message, 'success'); 23454 23455 // Re-enable button 23456 helpfulBtn.disabled = false; 23457 } else { 23458 showToast(data.message, 'error'); 23459 helpfulBtn.disabled = false; 23460 } 23461 23462 } catch (error) { 23463 console.error('Mark helpful error:', error); 23464 showToast('Gagal memberikan vote. Silakan coba lagi.', 'error'); 23465 helpfulBtn.disabled = false; 23466 } 23467 } 23468 23469 /** 23470 * Show review submission modal (placeholder) 23471 */ 23472 function showReviewModal() { 23473 // TODO: Implement review modal 23474 // For now, redirect to account page 23475 window.location.href = '/account/orders'; 23476 } 23477 23478 /** 23479 * Show toast notification 23480 */ 23481 function showToast(message, type = 'info') { 23482 const toast = document.createElement('div'); 23483 toast.className = `review-toast toast-${type}`; 23484 toast.textContent = message; 23485 23486 document.body.appendChild(toast); 23487 23488 setTimeout(() => { 23489 toast.classList.add('show'); 23490 }, 100); 23491 23492 setTimeout(() => { 23493 toast.classList.remove('show'); 23494 setTimeout(() => toast.remove(), 300); 23495 }, 3000); 23496 } 23497 23498 /** 23499 * Show modal dialog 23500 * @param {Object} options - Modal options 23501 * @param {string} options.title - Modal title 23502 * @param {string} options.message - Modal message 23503 * @param {string} options.icon - Icon type (info, success, warning, danger) 23504 * @param {Array} options.buttons - Array of button objects {text, className, callback} 23505 */ 23506 function showModal(options) { 23507 const {
23508 title = 'Konfirmasi', 23509 message = '', 23510 icon = 'info', 23511 buttons = [{ text: 'OK', className: 'modal-btn-primary' }] 23512 } = options; 23513 23514 // Icon emoji mapping 23515 const iconEmojis = { 23516 info: 'â¹ï¸', 23517 success: 'â', 23518 warning: 'â ï¸', 23519 danger: 'â' 23520 }; 23521 23522 // Create modal HTML 23523 const modalHTML = ` 23524 <div class="modal-overlay" id="helpful-vote-modal" style="display: flex !important; align-items: center !important; justify-content: center !important;"> 23525 <div class="modal-dialog"> 23526 <div class="modal-header"> 23527 <h3 class="modal-title"> 23528 <span class="modal-icon ${icon}">${iconEmojis[icon] || iconEmojis.info}</span> 23529 ${escapeHtml(title)} 23530 </h3> 23531 </div> 23532 <div class="modal-body"> 23533 <p class="modal-message">${escapeHtml(message)}</p> 23534 <div class="modal-actions"> 23535 ${buttons.map((btn, index) => 23536 `<button class="modal-btn ${btn.className || 'modal-btn-secondary'}" data-index="${index}">${escapeHtml(btn.text)}</button>` 23537 ).join('')} 23538 </div> 23539 </div> 23540 </div> 23541 </div> 23542 `; 23543 23544 // Append to body 23545 const tempDiv = document.createElement('div'); 23546 tempDiv.innerHTML = modalHTML; 23547 const modal = tempDiv.firstElementChild; 23548 document.body.appendChild(modal); 23549 23550 // Add event listeners to buttons 23551 buttons.forEach((btn, index) => { 23552 const btnElement = modal.querySelector(`[data-index="${index}"]`); 23553 btnElement.addEventListener('click', () => { 23554 closeModal(modal); 23555 if (btn.callback && typeof btn.callback === 'function') { 23556 btn.callback(); 23557 } 23558 }); 23559 }); 23560 23561 // Close on overlay click 23562 modal.addEventListener('click', (e) => { 23563 if (e.target === modal) { 23564 closeModal(modal); 23565 } 23566 }); 23567 23568 // Show modal with animation 23569 setTimeout(() => modal.classList.add('show'), 10); 23570 } 23571 23572 /** 23573 * Close modal dialog 23574 */ 23575 function closeModal(modal) { 23576 modal.classList.remove('show'); 23577 setTimeout(() => modal.remove(), 200); 23578 } 23579 23580 /** 23581 * Escape HTML for XSS protection 23582 */ 23583 function escapeHtml(text) { 23584 const div = document.createElement('div'); 23585 div.textContent = text; 23586 return div.innerHTML; 23587 } 23588 23589 /** 23590 * Get product ID from URL 23591 */ 23592 function getProductIdFromUrl() { 23593 const match = window.location.pathname.match(/\/product\/(\d+)/); 23594 return match ? match[1] : null; 23595 } 23596 23597 // Public API 23598 return { 23599 init: init 23600 }; 23601 23602})(); 23603 23604</script>
23604 23605
23605<script data-inlined="review-submission.js"> 23606/** 23607 * Review Submission Modal & Handler 23608 * Handles review form display, validation, and submission 23609 */ 23610 23611(function() { 23612 'use strict'; 23613 23614 let currentProductId = null; 23615 let currentOrderId = null; 23616 let currentSoRefId = null; 23617 let selectedRating = 0; 23618 let uploadedImages = []; 23619 23620 // Get CSRF token from meta tag 23621 function getCsrfToken() { 23622 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23623 return metaTag ? metaTag.getAttribute('content') : ''; 23624 } 23625 23626 // Initialize 23627 function init() { 23628 createReviewModal(); 23629 attachEventListeners(); 23630 loadReviewableOrders(); 23631 } 23632 23633 // Create review modal HTML 23634 function createReviewModal() { 23635 const modalHTML = ` 23636 <div class="modal-overlay" id="review-modal-overlay" style="display:none;"> 23637 <div class="modal-content review-modal"> 23638 <div class="modal-header"> 23639 <h3 class="modal-title">Tulis Ulasan</h3> 23640 <button type="button" class="modal-close" id="review-modal-close" aria-label="Tutup"> 23641 <svg width="20" height="20" viewBox="0 0 24 24"> 23642 <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"/> 23643 </svg> 23644 </button> 23645 </div> 23646 23647 <div class="modal-body"> 23648 <form id="review-form" class="review-form"> 23649 <!-- Product Info --> 23650 <div class="review-product-info" id="review-product-info"> 23651 <img id="review-product-image" src="" alt="" class="review-product-thumb"> 23652 <div class="review-product-details"> 23653 <h4 id="review-product-name"></h4> 23654 <p id="review-product-sku"></p> 23655 </div> 23656 </div> 23657 23658 <!-- Rating --> 23659 <div class="form-group"> 23660 <label class="form-label">Rating <span style="color:#ef4444;">*</span></label> 23661 <div class="star-rating-input" id="star-rating-input"> 23662 <button type="button" class="star-btn" data-rating="1">â </button> 23663 <button type="button" class="star-btn" data-rating="2">â </button> 23664 <button type="button" class="star-btn" data-rating="3">â </button> 23665 <button type="button" class="star-btn" data-rating="4">â </button> 23666 <button type="button" class="star-btn" data-rating="5">â </button> 23667 </div> 23668 <p class="rating-label" id="rating-label">Pilih rating Anda</p> 23669 </div> 23670 23671 <!-- Review Text --> 23672 <div class="form-group"> 23673 <label class="form-label" for="review-text">Ulasan Anda <span style="color:#ef4444;">*</span></label> 23674 <textarea 23675 id="review-text" 23676 name="review_text" 23677 class="form-control" 23678 rows="6" 23679 placeholder="Ceritakan pengalaman Anda dengan produk ini..." 23680 minlength="20" 23681 maxlength="5000" 23682 required></textarea> 23683 <p class="form-hint"> 23684 <span id="review-char-count">0</span>/5000 karakter
23685 <span style="color:#059669;font-weight:500;"> 23686 ð° +1,000 point untuk ulasan ⥠100 karakter 23687 </span> 23688 </p> 23689 </div> 23690 23691 <!-- Image Upload --> 23692 <div class="form-group"> 23693 <label class="form-label">Upload Foto (Opsional)</label> 23694 <div class="image-upload-container"> 23695 <input type="file" 23696 id="review-images" 23697 name="images[]" 23698 accept="image/jpeg,image/jpg,image/png,image/webp" 23699 multiple 23700 style="display:none;"> 23701 <button type="button" class="btn-upload-trigger" id="upload-trigger"> 23702 <svg width="24" height="24" viewBox="0 0 24 24"> 23703 <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"/> 23704 </svg> 23705 <span>Pilih Foto</span> 23706 </button> 23707 <p class="form-hint"> 23708 Max 5 foto, format JPG/PNG/WebP, max 2MB per foto 23709 <span style="color:#059669;font-weight:500;"> 23710 ð° +2,000 point untuk ulasan dengan foto 23711 </span> 23712 </p> 23713 </div> 23714 <div class="image-preview-grid" id="image-preview-grid"></div> 23715 </div> 23716 23717 <!-- Point Reward Info --> 23718 <div class="point-reward-info"> 23719 <div class="pri-header"> 23720 <span>ð</span> 23721 <span>Dapatkan Point Reward</span> 23722 </div> 23723 <ul class="pri-list"> 23724 <li>â Ulasan dasar: <strong>5,000 point</strong></li> 23725 <li id="pri-long-text" style="opacity:0.5;">ð Ulasan panjang (â¥100 kata): <strong>+1,000 point</strong></li> 23726 <li id="pri-images" style="opacity:0.5;">ð¸ Dengan foto: <strong>+2,000 point</strong></li> 23727 </ul> 23728 <div class="pri-total"> 23729 Total Reward: <strong id="pri-total-points">5,000 point</strong> 23730 </div> 23731 </div> 23732 23733 <!-- Privacy Option --> 23734 <div class="form-group" style="margin-top:20px;padding:15px;background:#f8fafc;border-radius:8px;"> 23735 <label class="checkbox-label" style="display:flex;align-items:center;cursor:pointer;margin:0;"> 23736 <input type="checkbox" 23737 id="review-anonymous" 23738 name="is_anonymous" 23739 style="margin-right:8px;width:18px;height:18px;cursor:pointer;"> 23740 <span style="font-size:14px;"> 23741 <strong>ð Sembunyikan nama saya</strong> 23742 <span style="color:#64748b;display:block;font-size:13px;margin-top:4px;" id="review-anonymous-preview"> 23743 Nama Anda akan ditampilkan de
23743ngan disamarkan untuk menjaga privasi 23744 </span> 23745 </span> 23746 </label> 23747 </div> 23748 23749 <!-- Error Alert --> 23750 <div class="form-error" id="review-form-error" style="display:none;"></div> 23751 23752 <!-- Submit Buttons --> 23753 <div class="modal-footer"> 23754 <button type="button" class="btn btn-secondary" id="review-cancel">Batal</button> 23755 <button type="submit" class="btn btn-primary" id="review-submit"> 23756 Kirim Ulasan 23757 </button> 23758 </div> 23759 </form> 23760 </div> 23761 </div> 23762 </div> 23763 `; 23764 23765 document.body.insertAdjacentHTML('beforeend', modalHTML); 23766 } 23767 23768 // Attach event listeners 23769 function attachEventListeners() { 23770 // Modal triggers - event delegation 23771 document.addEventListener('click', function(e) { 23772 const btn = e.target.closest('.btn-write-review'); 23773 if (btn) { 23774 e.preventDefault(); 23775 openReviewModal( 23776 btn.dataset.productId, 23777 btn.dataset.orderId, 23778 btn.dataset.productName, 23779 btn.dataset.productImage, 23780 btn.dataset.productSku, 23781 btn.dataset.soRefId || null 23782 ); 23783 } 23784 }); 23785 23786 // Close modal 23787 setTimeout(() => { 23788 const closeBtn = document.getElementById('review-modal-close'); 23789 const cancelBtn = document.getElementById('review-cancel'); 23790 const overlay = document.getElementById('review-modal-overlay'); 23791 23792 if (closeBtn) { 23793 closeBtn.addEventListener('click', closeReviewModal); 23794 } 23795 if (cancelBtn) { 23796 cancelBtn.addEventListener('click', closeReviewModal); 23797 } 23798 if (overlay) { 23799 overlay.addEventListener('click', function(e) { 23800 if (e.target === overlay) closeReviewModal(); 23801 }); 23802 } 23803 }, 100); 23804 23805 // Star rating 23806 setTimeout(() => { 23807 const starButtons = document.querySelectorAll('.star-btn'); 23808 starButtons.forEach(btn => { 23809 btn.addEventListener('click', function(e) { 23810 e.preventDefault(); 23811 const rating = parseInt(this.dataset.rating); 23812 setRating(rating); 23813 }); 23814 }); 23815 }, 100); 23816 23817 // Character count 23818 setTimeout(() => { 23819 const textarea = document.getElementById('review-text'); 23820 if (textarea) { 23821 textarea.addEventListener('input', function() { 23822 updateCharCount(); 23823 calculatePotentialPoints(); 23824 }); 23825 } 23826 }, 100); 23827 23828 // Image upload 23829 setTimeout(() => { 23830 const uploadTrigger = document.getElementById('upload-trigger'); 23831 const fileInput = document.getElementById('review-images'); 23832 23833 if (uploadTrigger && fileInput) { 23834 uploadTrigger.addEventListener('click', () => fileInput.click()); 23835 fileInput.addEventListener('change', handleImageSelection); 23836 } 23837 }, 100); 23838 23839 // Form submit 23840 setTimeout(() => { 23841 const form = document.getElementById('review-form'); 23842 if (form) { 23843 form.addEventListener('submit', handleSubmit); 23844 } 23845 }, 100); 23846 } 23847 23848 // Open review modal 23849 function openReviewModal(productId, orderId, productName, productImage, productSku, soRefId = null) { 23850 const overlay = document.getElementById('review-modal-overlay'); 23851 if (!overlay) { 23852 console.error('Modal overlay not found!'); 23853 return; 23854 } 23855 23856 currentProductId = productId; 23857 currentOrderId = orderId; 23858 currentSoRefId = soRefId; 23859 selectedRating = 0; 23860 uploadedImages = []; 23861 23862 // Set product info 23863 const nameEl = document.getElementById('review-product-name'); 23864 const imageEl = document.getElementById('review-product-image'); 23865 const skuEl = document.getElementById('review-product-sku'); 23866 23867 if (nameEl) nameEl.textContent = productName; 23868 if (imageEl) imageEl.src = productImage || '/assets//img/no-image.png'; 23869 if (skuEl) skuEl.textContent = 'SKU: ' + productSku; 23870 23871 // Update privacy preview with actual user name 23872 updatePrivacyPreview(); 23873 23874 // Reset form 23875 const form = document.getElementById('review-form'); 23876 const previewGrid = document.getElementById('image-preview-grid'); 23877 const errorEl = document.getElementById('review-form-error'); 23878 23879 if (form) form.reset(); 23880 if (previewGrid) previewGrid.innerHTML = ''; 23881 if (errorEl) errorEl.style.display = 'none'; 23882 23883 setRating(0); 23884 updateCharCount(); 23885 calculatePotentialPoints();
23886 23887 // Show modal 23888 overlay.style.display = 'flex'; 23889 overlay.classList.add('show'); 23890 document.body.style.overflow = 'hidden'; 23891 } 23892 23893 // Close review modal 23894 function closeReviewModal() { 23895 const overlay = document.getElementById('review-modal-overlay'); 23896 if (overlay) { 23897 overlay.classList.remove('show'); 23898 setTimeout(() => { 23899 overlay.style.display = 'none'; 23900 }, 200); // Wait for fade animation 23901 } 23902 document.body.style.overflow = ''; 23903 } 23904 23905 // Set star rating 23906 function setRating(rating) { 23907 selectedRating = rating; 23908 const stars = document.querySelectorAll('.star-btn'); 23909 const labels = ['', 'Sangat Buruk', 'Buruk', 'Cukup', 'Bagus', 'Sangat Bagus']; 23910 23911 stars.forEach((star, index) => { 23912 if (index < rating) { 23913 star.classList.add('active'); 23914 } else { 23915 star.classList.remove('active'); 23916 } 23917 }); 23918 23919 document.getElementById('rating-label').textContent = labels[rating] || 'Pilih rating Anda'; 23920 } 23921 23922 // Update character count 23923 function updateCharCount() { 23924 const textarea = document.getElementById('review-text'); 23925 const count = textarea.value.length; 23926 document.getElementById('review-char-count').textContent = count; 23927 } 23928 23929 // Calculate potential points 23930 function calculatePotentialPoints() { 23931 const textarea = document.getElementById('review-text'); 23932 if (!textarea) return; 23933 23934 const charCount = textarea.value.trim().length; 23935 const hasImages = uploadedImages.length > 0; 23936 23937 let totalPoints = 5000; 23938 let longTextActive = charCount >= 100; 23939 let imagesActive = hasImages; 23940 23941 if (longTextActive) totalPoints += 1000; 23942 if (imagesActive) totalPoints += 2000; 23943 23944 // Update UI 23945 const longTextEl = document.getElementById('pri-long-text'); 23946 const imagesEl = document.getElementById('pri-images'); 23947 const totalEl = document.getElementById('pri-total-points'); 23948 23949 if (longTextEl) longTextEl.style.opacity = longTextActive ? '1' : '0.5'; 23950 if (imagesEl) imagesEl.style.opacity = imagesActive ? '1' : '0.5'; 23951 if (totalEl) totalEl.textContent = totalPoints.toLocaleString('id-ID') + ' point'; 23952 } 23953 23954 // Handle image selection 23955 function handleImageSelection(e) { 23956 const files = Array.from(e.target.files); 23957 23958 if (files.length > 5) { 23959 showError('Maksimal 5 foto yang dapat diupload'); 23960 e.target.value = ''; // Reset input 23961 return; 23962 } 23963 23964 uploadedImages = []; 23965 const previewGrid = document.getElementById('image-preview-grid'); 23966 previewGrid.innerHTML = ''; 23967 23968 files.forEach((file, index) => { 23969 // Validate file type 23970 if (!file.type.match(/image\/(jpeg|jpg|png|webp)/)) { 23971 showError('Format file harus JPG, PNG, atau WebP'); 23972 return; 23973 } 23974 23975 // Validate file size (2MB) 23976 if (file.size > 2 * 1024 * 1024) { 23977 showError('Ukuran file maksimal 2MB'); 23978 return; 23979 } 23980 23981 uploadedImages.push(file); 23982 23983 // Create preview 23984 const reader = new FileReader(); 23985 reader.onload = function(e) { 23986 const previewHTML = ` 23987 <div class="image-preview-item"> 23988 <img src="${e.target.result}" alt="Preview ${index + 1}"> 23989 <button type="button" class="remove-image" data-index="${index}" aria-label="Hapus"> 23990 <svg width="16" height="16" viewBox="0 0 24 24"> 23991 <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"/> 23992 </svg> 23993 </button> 23994 </div> 23995 `; 23996 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 23997 };
23998 reader.readAsDataURL(file); 23999 }); 24000 24001 calculatePotentialPoints(); 24002 24003 // Attach remove handlers 24004 setTimeout(() => { 24005 document.querySelectorAll('.remove-image').forEach(btn => { 24006 btn.addEventListener('click', function() { 24007 const index = parseInt(this.dataset.index); 24008 removeImage(index); 24009 }); 24010 }); 24011 }, 100); 24012 } 24013 24014 // Remove image 24015 function removeImage(index) { 24016 uploadedImages.splice(index, 1); 24017 24018 const previewGrid = document.getElementById('image-preview-grid'); 24019 previewGrid.innerHTML = ''; 24020 24021 uploadedImages.forEach((file, idx) => { 24022 const reader = new FileReader(); 24023 reader.onload = function(e) { 24024 const previewHTML = ` 24025 <div class="image-preview-item"> 24026 <img src="${e.target.result}" alt="Preview ${idx + 1}"> 24027 <button type="button" class="remove-image" data-index="${idx}" aria-label="Hapus"> 24028 <svg width="16" height="16" viewBox="0 0 24 24"> 24029 <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"/> 24030 </svg> 24031 </button> 24032 </div> 24033 `; 24034 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24035 }; 24036 reader.readAsDataURL(file); 24037 }); 24038 24039 calculatePotentialPoints(); 24040 24041 setTimeout(() => { 24042 document.querySelectorAll('.remove-image').forEach(btn => { 24043 btn.addEventListener('click', function() { 24044 const idx = parseInt(this.dataset.index); 24045 removeImage(idx); 24046 }); 24047 }); 24048 }, 100); 24049 } 24050 24051 // Handle form submission 24052 async function handleSubmit(e) { 24053 e.preventDefault(); 24054 24055 // Validate 24056 if (selectedRating === 0) { 24057 showError('Silakan pilih rating terlebih dahulu'); 24058 return; 24059 } 24060 24061 const reviewText = document.getElementById('review-text').value.trim(); 24062 if (reviewText.length < 20) { 24063 showError('Ulasan minimal 20 karakter'); 24064 return; 24065 } 24066 24067 const submitBtn = document.getElementById('review-submit'); 24068 submitBtn.disabled = true; 24069 24070 try { 24071 // Step 1: Upload images if any 24072 let imageUrls = []; 24073 if (uploadedImages.length > 0) { 24074 submitBtn.textContent = 'Mengupload foto...'; 24075 imageUrls = await uploadImages(); 24076 if (!imageUrls) { 24077 // Upload failed 24078 submitBtn.disabled = false; 24079 submitBtn.textContent = 'Kirim Ulasan'; 24080 return; 24081 } 24082 } 24083 24084 // Step 2: Submit review with image URLs 24085 submitBtn.textContent = 'Mengirim ulasan...'; 24086 24087 const payload = { 24088 csrf_token: getCsrfToken(), 24089 product_id: currentProductId, 24090 order_id: currentOrderId || 0, 24091 so_ref_id: currentSoRefId || undefined, 24092 rating: selectedRating, 24093 review_text: reviewText, 24094 is_anonymous: document.getElementById('review-anonymous').checked, 24095 images: imageUrls 24096 }; 24097 24098 const response = await fetch('/api/reviews/submit.php', { 24099 method: 'POST', 24100 headers: { 24101 'Content-Type': 'application/json', 24102 'X-CSRF-Token': getCsrfToken() 24103 }, 24104 body: JSON.stringify(payload) 24105 }); 24106 24107 const result = await response.json(); 24108 24109 if (result.success) { 24110 showSuccess(result.message || 'Ulasan berhasil dikirim!'); 24111 closeReviewModal(); 24112 24113 // Refresh reviewable orders if on account page 24114 if (typeof loadReviewableOrders === 'function') { 24115 setTimeout(loadReviewableOrders, 500); 24116 } 24117 } else { 24118 showError(result.message || 'Gagal mengirim ulasan'); 24119 } 24120 } catch (error) { 24121 console.error('Submit error:', error); 24122 showError('Terjadi kesalahan saat mengirim ulasan'); 24123 } finally { 24124 submitBtn.disabled = false; 24125 submitBtn.textContent = 'Kirim Ulasan'; 24126 } 24127 } 24128 24129 /** 24130 * Upload images to server 24131 * @returns {Promise<Array|null>} Array of image URLs or null if failed 24132 */ 24133 async function uploadImages() { 24134 if (uploadedImages.length === 0) { 24135 return []; 24136 } 24137 24138 const formData = new FormData();
24139 uploadedImages.forEach(file => { 24140 formData.append('images[]', file); 24141 }); 24142 24143 try { 24144 const response = await fetch('/api/reviews/upload_images.php', { 24145 method: 'POST', 24146 headers: { 24147 'X-CSRF-Token': getCsrfToken() 24148 }, 24149 body: formData 24150 }); 24151 24152 // Check if response is JSON 24153 const contentType = response.headers.get('content-type'); 24154 if (!contentType || !contentType.includes('application/json')) { 24155 const text = await response.text(); 24156 console.error('Non-JSON response:', text); 24157 showError('Server error: Response bukan JSON. Cek browser console untuk detail.'); 24158 return null; 24159 } 24160 24161 const result = await response.json(); 24162 24163 if (result.success && result.images) { 24164 // Extract URLs from uploaded images 24165 return result.images.map(img => img.url); 24166 } else { 24167 showError(result.message || 'Gagal mengupload foto'); 24168 return null; 24169 } 24170 } catch (error) { 24171 console.error('Upload images error:', error); 24172 showError('Terjadi kesalahan saat mengupload foto. Cek console untuk detail.'); 24173 return null; 24174 } 24175 } 24176 24177 // Load reviewable orders (for account page) 24178 async function loadReviewableOrders() { 24179 // Only run if on account/profile page 24180 const container = document.getElementById('reviewable-orders-list'); 24181 const loading = document.getElementById('reviewable-orders-loading'); 24182 const empty = document.getElementById('reviewable-orders-empty'); 24183 24184 if (!container) { 24185 return; 24186 } 24187 24188 // Show loading 24189 if (loading) loading.style.display = 'block'; 24190 if (empty) empty.style.display = 'none'; 24191 container.innerHTML = ''; 24192 24193 try { 24194 const response = await fetch('/api/reviews/reviewable.php'); 24195 const result = await response.json(); 24196 24197 if (loading) loading.style.display = 'none'; 24198 24199 if (result.success && result.data.length > 0) { 24200 renderReviewableOrders(result.data); 24201 if (empty) empty.style.display = 'none'; 24202 } else { 24203 if (empty) empty.style.display = 'block'; 24204 } 24205 } catch (error) { 24206 console.error('Load reviewable orders error:', error); 24207 if (loading) loading.style.display = 'none'; 24208 if (empty) { 24209 empty.innerHTML = '<p>Gagal memuat data. Silakan refresh halaman.</p>'; 24210 empty.style.display = 'block'; 24211 } 24212 } 24213 } 24214 24215 // Render reviewable orders 24216 function renderReviewableOrders(orders) { 24217 const container = document.getElementById('reviewable-orders-list'); 24218 if (!container) return; 24219 24220 let html = ''; 24221 24222 orders.forEach(order => { 24223 html += ` 24224 <div class="reviewable-order-item"> 24225 <img src="${escapeHtml(order.product_image || '/assets//img/no-image.png')}" alt="${escapeHtml(order.product_name)}" class="roi-image"> 24226 <div class="roi-info"> 24227 <h4>${escapeHtml(order.product_name)}</h4> 24228 <p class="roi-meta">Order: ${escapeHtml(order.order_number)}</p> 24229 <p class="roi-meta">Dibeli: ${escapeHtml(order.order_date)}</p> 24230 </div> 24231 <button type="button" 24232 class="btn btn-primary btn-write-review" 24233 data-product-id="${order.product_id}" 24234 data-order-id="${order.order_id || ''}" 24235 data-so-ref-id="${escapeHtml(order.so_ref_id || '')}" 24236 data-product-name="${escapeHtml(order.product_name)}" 24237 data-product-image="${escapeHtml(order.product_image || '')}" 24238 data-product-sku="${escapeHtml(order.product_sku || '')}"> 24239 âï¸ Tulis Ulasan 24240 </button> 24241 </div> 24242 `; 24243 }); 24244 24245 container.innerHTML = html; 24246 } 24247 24248 // Escape HTML for XSS protection 24249 function escapeHtml(text) { 24250 const div = document.createElement('div'); 24251 div.textContent = text; 24252 return div.innerHTML; 24253 } 24254 24255 // Show error 24256 function showError(message) { 24257 const errorEl = document.getElementById('review-form-error'); 24258 errorEl.textContent = message; 24259 errorEl.style.display = 'block'; 24260 24261 setTimeout(() => { 24262 errorEl.style.display = 'none'; 24263 }, 5000); 24264 } 24265 24266 // Show success 24267 function showSuccess(message) { 24268 // Create toast notification 24269 const toast = document.createElement('div'); 24270 toast.className = 'toast-notification success'; 24271 toast.textContent = message; 24272 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;'; 24273 24274 document.body.appendChild(toast); 24275 24276 setTimeout(() => { 24277 toast.style.opacity = '0'; 24278 toast.style.transition = 'opacity 0.3s'; 24279 setTimeout(() => toast.remove(), 300); 24280 }, 3000); 24281 } 24282 24283 // Mask user name for privacy preview (like PHP maskReviewerName) 24284 function maskUserName(fullName) { 24285 if (!fullName || fullName.trim() === '') { 24286 return 'Anonymous'; 24287 } 24288 24289 const words = fullName.trim().split(/\s+/); 24290 const maskedWords = words.map(word => { 24291 const len = word.length; 24292 if (len <= 1) return word; 24293 return word.charAt(0) + '*'.repeat(len - 1); 24294 }); 24295 24296 return maskedWords.join(' '); 24297 } 24298 24299 // Update privacy preview text with user's actual masked name 24300 function updatePrivacyPreview() { 24301 const previewEl = document.getElementById('review-anonymous-preview'); 24302 if (!previewEl) return; 24303 24304 const userName = window.userFullName || ''; 24305 if (userName) { 24306 const maskedName = maskUserName(userName); 24307 previewEl.textContent = `Nama Anda akan ditampilkan sebagai "${maskedName}" untuk menjaga privasi`; 24308 } else { 24309 previewEl.textContent = 'Nama Anda akan ditampilkan de
24309ngan disamarkan untuk menjaga privasi'; 24310 } 24311 } 24312 24313 // Initialize when DOM ready 24314 if (document.readyState === 'loading') { 24315 document.addEventListener('DOMContentLoaded', init); 24316 } else { 24317 init(); 24318 } 24319 24320 // Export openReviewModal to global scope for use in other scripts 24321 window.openReviewModal = openReviewModal; 24322 24323})(); 24324 24325</script>
24325 24326 <style> 24327.slot-top-fixed-footer{ 24328 position: fixed; 24329 left: 0; 24330 right: 0; 24331 bottom: 0; 24332 z-index: 999; 24333 display: flex; 24334 align-items: center; 24335 justify-content: space-around; 24336 gap: 8px; 24337 padding: 11px 12px calc(11px + env(safe-area-inset-bottom)); 24338 overflow: hidden; 24339 background: radial-gradient(circle at 50% 0%, #000000, rgb(0 0 0 / 92%) 58%, rgb(0 0 0 / 98%)), linear-gradient(180deg, rgba(11, 26, 51, 0.92), rgb(0 0 0 / 98%)); 24340 backdrop-filter: blur(12px); 24341 border-top: 1px solid rgb(234 179 8); 24342 border-radius: 18px 18px 0 0; 24343 box-shadow: 0 -12px 30px #0B1A33, inset 0 1px 0 rgba(255, 255, 255, .05); 24344 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 24345} 24346 24347.slot-top-fixed-footer::before{ 24348 content:""; 24349 position:absolute; 24350 inset:0; 24351 pointer-events:none; 24352 background: 24353 repeating-linear-gradient( 24354 0deg, 24355 rgba(255,255,255,.015), 24356 rgba(255,255,255,.015) 1px, 24357 transparent 1px, 24358 transparent 6px 24359 ); 24360 opacity:.85; 24361 mix-blend-mode:overlay; 24362} 24363 24364.slot-top-fixed-footer::after{ 24365 content:""; 24366 position:absolute; 24367 top:0; 24368 left:-160%; 24369 width:60%; 24370 height:100%; 24371 background: linear-gradient(120deg, transparent, rgba(251, 191, 36, 0.25), transparent); 24372 transform: skewX(-25deg); 24373 animation: footerFinalShine 3.8s infinite; 24374 mix-blend-mode: screen; 24375 pointer-events:none; 24376} 24377 24378@keyframes footerFinalShine{ 24379 0%{left:-160%;} 24380 68%{left:160%;} 24381 100%{left:160%;} 24382} 24383 24384.slot-top-fixed-footer a{ 24385 flex:1; 24386 max-width:110px; 24387 display:flex; 24388 flex-direction:column; 24389 align-items:center; 24390 justify-content:center; 24391 gap:6px; 24392 padding:8px 6px; 24393 text-decoration:none; 24394 text-align:center; 24395 color:#D4AF37; 24396 font-size:12px; 24397 font-weight:800; 24398 letter-spacing:.25px; 24399 border-radius:14px; 24400 position:relative; 24401 z-index:1; 24402 transition:all .18s ease; 24403} 24404 24405.slot-top-fixed-footer a:hover{ 24406 color:#ffffff; 24407 background:rgba(212,175,55,.1); 24408 filter:drop-shadow(0 0 12px rgba(212,175,55,.2)); 24409 transform:translateY(-1px); 24410} 24411 24412.slot-top-fixed-footer a:active{ 24413 transform:translateY(0) scale(.99); 24414} 24415 24416.slot-top-fixed-footer img, 24417.slot-top-fixed-footer .footer-nav-icon{ 24418 width:26px; 24419 height:26px; 24420 transition:.18s ease; 24421 filter:drop-shadow(0 0 8px rgba(234,179,8,.35)); 24422 flex-shrink:0; 24423} 24424 24425.slot-top-fixed-footer .footer-nav-icon svg{ 24426 width:100%; 24427 height:100%; 24428 display:block; 24429} 24430 24431.slot-top-fixed-footer a:hover img, 24432.slot-top-fixed-footer a:hover .footer-nav-icon{ 24433 transform:scale(1.08); 24434 filter:drop-shadow(0 0 16px rgba(251,191,36,.6)); 24435} 24436 24437.slot-top-fixed-footer .hud-corner{ 24438 position:absolute; 24439 width:18px; 24440 height:18px; 24441 pointer-events:none; 24442 border:1px solid rgba(234,179,8,.5); 24443 filter:drop-shadow(0 0 10px rgba(234,179,8,.2)); 24444 opacity:.85; 24445} 24446 24447.slot-top-fixed-footer .hud-tl{top:8px;left:10px;border-right:none;border-bottom:none;border-radius:10px 0 0 0;} 24448.slot-top-fixed-footer .hud-tr{top:8px;right:10px;border-left:none;border-bottom:none;border-radius:0 10px 0 0;} 24449.slot-top-fixed-footer .hud-bl{bottom:8px;left:10px;border-right:none;border-top:none;border-radius:0 0 0 10px;} 24450.slot-top-fixed-footer .hud-br{bottom:8px;right:10px;border-left:none;border-top:none;border-radius:0 0 10px 0;} 24451 24452.slot-top-fixed-footer .tada{ 24453 color:#1a1400; 24454 background:linear-gradient(180deg, #F5E6A8 0%, #D4AF37 40%, #C9A227 70%, #8B6914 100%); 24455 border:1px solid rgba(212,175,55,.55); 24456 box-shadow:0 0 18px rgba(212,175,55,.45), inset 0 1px 0 rgba(255,248,220,.4), inset 0 -1px 0 rgba(0,0,0,.2); 24457} 24458 24459.slot-top-fixed-footer .tada img{ 24460 width:30px; 24461 height:30px; 24462 filter:drop-shadow(0 0 12px rgba(212,175,55,.55)); 24463} 24464 24465.slot-top-fixed-footer .tada::before{ 24466 content:""; 24467 position:absolute; 24468 inset:-12px; 24469 border-radius:999px; 24470 background: 24471 radial-gradient(circle,#000000,transparent 70%), 24472 radial-gradient(circle,rgba(251,191,36,.08),transparent 72%); 24473 filter:blur(12px); 24474 opacity:.55; 24475 animation:corePulse 2.9s ease-in-out infinite; 24476 z-index:-1; 24477} 24478 24479@keyframes corePulse{ 24480 0%,100%{opacity:.35;transform:scale(.99);} 24481 50%{opacity:.75;transform:scale(1.02);} 24482} 24483 24484.slot-top-fixed-footer a.is-active{ 24485 color:#ffffff; 24486 background:rgba(234,179,8,.1); 24487 box-shadow:inset 0 0 0 1px rgba(234,179,8,.25); 24488} 24489 24490.slot-top-fixed-footer a.is-active img{ 24491 filter:drop-shadow(0 0 18px rgba(251,191,36,.8)); 24492} 24493 24494@media (max-width:500px){ 24495 .slot-top-fixed-footer{ 24496 padding:10px 10px calc(10px + env(safe-area-inset-bottom)); 24497 border-radius:16px 16px 0 0; 24498 gap:6px; 24499 } 24500 24501 .slot-top-fixed-footer a{ 24502 font-size:11px; 24503 padding:7px 5px; 24504 gap:5px; 24505 max-width:104px; 24506 } 24507 24508 .slot-top-fixed-footer img{ 24509 width:22px; 24510 height:22px; 24511 } 24512 24513 .slot-top-fixed-footer .tada img{ 24514 width:26px; 24515 height:26px; 24516 } 24517} 24518</style> 24519<div class="slot-top-fixed-footer"> 24520 <a href="https://relaksasi.norita.mx/" target="_blank" rel="nofollow noopener"> 24521 <img src="/img/favicon.png" alt="BONUS EDUTOTO"> 24522 Promosi 24523 </a> 24524 <a href="https://relaksasi.norita.mx/" target="_blank" rel="nofollow noopener"> 24525 <img src="/img/favicon.png" alt="LOGIN EDUTOTO"> 24526 Login 24527 </a> 24528 <a href="https://relaksasi.norita.mx/" target="_blank" rel="nofollow noopener" class="tada">
24529 <img src="/img/favicon.png" alt="DAFTAR EDUTOTO"> 24530 Daftar 24531 </a> 24532 <a href="https://relaksasi.norita.mx/" target="_blank" rel="nofollow noopener"> 24533 <img src="/img/favicon.png" alt="RTP EDUTOTO"> 24534 RTP 24535 </a> 24536 <a href="https://relaksasi.norita.mx/" target="_blank" rel="nofollow noopener"> 24537 <img src="/img/favicon.png" alt="LIVE CHAT EDUTOTO"> 24538 Live Chat 24539 </a> 24540</div>
24541<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"d2b233a7815a4287aaa052abb87bc709","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 24541
24541
24542<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"a53b173d9c24451dab6bb5be1849fbba","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 24542
24542
24543<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"89fcf4ab9804493f841f6843fd186fff","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 24543
24543
24544<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"a879294c6a484e56a7b1c10ea9ba2706","r":1,"spa":2}' crossorigin="anonymous"></script>
24544 24545</body> 24546</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.