1 2<!DOCTYPE html> 3<html lang="id"> 4<head> 5 <meta charset="UTF-8"> 6 <title>Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi</title> 7 <meta name="description" content="BERLINTOTO hadir sebagai platform hiburan digital serba ada yang mengintegrasikan permainan tebak angka presisi dan slot dengan tingkat kemenangan tinggi."> 8 <meta name="robots" content="index,follow"> 9 <link rel="canonical" href="https://www.bim.miami/"> 10 <meta name="google-site-verification" content="GdDmVTk4T-oneBYNmEZWUzkor6JveNfrJIvtzHVclPQ" /> 11 <link rel="amphtml" href="https://bim.miami/aboutus/"> 12 <link rel="alternate" href="https://bim.miami/aboutus/" hreflang="id"> 13 <link rel="alternate" href="https://bim.miami/aboutus/" hreflang="en"> 14 <link rel="alternate" href="https://bim.miami/aboutus/" hreflang="x-default"> 15 <link rel="alternate" hreflang="id-ID" href="https://bim.miami/aboutus/" /> 16 <link rel="alternate" hreflang="en-US" href="https://bim.miami/aboutus/" /> 17 <link rel="alternate" hreflang="en-SG" href="https://bim.miami/aboutus/" /> 18 <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
vendor: 66 bytes, lines 18-19
18 19 <script async src="https://www.googletagmanager.com/gtag/js?id=
19G-0LSM1YJSPZ
vendor: 47 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="TOTO TOGEL"> 23 <meta property="og:type" content="product"> 24 <meta property="og:title" content="Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi"> 25 <meta property="og:description" content="BERLINTOTO hadir sebagai platform hiburan digital serba ada yang mengintegrasikan permainan tebak angka presisi dan slot dengan tingkat kemenangan tinggi."> 26 <meta property="og:url" content="https://www.bim.miami/"> 27 <meta property="og:image" content="https://www.bim.miami/berlintoto.png"> 28 <meta property="og:image:alt" content="Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi"> 29<meta property="product:price:amount" content="5000"> 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="Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi"> 34<meta name="twitter:description" content="BERLINTOTO hadir sebagai platform hiburan digital serba ada yang mengintegrasikan permainan tebak angka presisi dan slot dengan tingkat kemenangan tinggi."> 35<meta name="twitter:image" content="https://www.bim.miami/berlintoto.png"> 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://www.bim.miami/", 46 "name": "Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi" 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": "Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi", 58 "description": "BERLINTOTO hadir sebagai platform hiburan digital serba ada yang mengintegrasikan permainan tebak angka presisi dan slot dengan tingkat kemenangan tinggi.", 59 "image": "https://www.bim.miami/berlintoto.png", 60 "offers": { 61 "@type": "http:\/\/schema.org\/Offer", 62 "price": 9999, 63 "url": "https://www.bim.miami/", 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": "Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi", 87"url": "https://www.bim.miami/", 88"sku": "BERLINTOTO", 89"image": [ 90 "https://www.bim.miami/berlintoto.png" 91], 92"description": "BERLINTOTO hadir sebagai platform hiburan digital serba ada yang mengintegrasikan permainan tebak angka presisi dan slot dengan tingkat kemenangan tinggi.", 93"brand": { 94 "@type": "Organization", 95 "name": "BERLIN TOTO" 96}, 97"offers": { 98 "@type": "Offer", 99 "name": "Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi", 100 "sku": "LOGIN BERLINTOTO", 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": "Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi", 113"image": "https://www.bim.miami/berlintoto.png", 114"description": "BERLINTOTO hadir sebagai platform hiburan digital serba ada yang mengintegrasikan permainan tebak angka presisi dan slot dengan tingkat kemenangan tinggi.", 115"sku": "DAFTAR BERLINTOTO", 116"offers": { 117 "@type": "Offer", 118 "priceCurrency": "IDR", 119 "url": "https://www.bim.miami/", 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://www.bim.miami/", 130"aggregateRating": { 131 "@type": "AggregateRating", 132 "itemReviewed": { 133 "@type": "IndividualProduct", 134 "name": "LINK BERLINTOTO" 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://www.bim.miami/", 153 "name": "BERLINTOTO" 154 } 155 }, 156 { 157 "@type": "ListItem", 158 "position": 2, 159 "item": { 160 "@id": "https://www.bim.miami/", 161 "name": "Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi" 162 } 163 }, 164 { 165 "@type": "ListItem", 166 "position": 3, 167 "item": { 168 "@id": "https://www.bim.miami/", 169 "name": "BERLIN TOTO" 170 } 171 }, 172 { 173 "@type": "ListItem", 174 "position": 4, 175 "item": { 176 "@id": "https://www.bim.miami/", 177 "name": "Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi" 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://www.bim.miami/", 189 "name": "BERLINTOTO", 190 "alternateName": "BANDAR BERLINTOTO", 191 "url": "https://www.bim.miami/", 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": "BERLINTOTO", 198 "reviewBody": "BERLINTOTO memudahkan saya membuka situs toto slot dan situs toto togel melalui satu portal utama. Menu yang tersedia mudah ditemukan sehingga saya bisa berpindah halaman dengan lebih sederhana saat menggunakan akun.", 199 "reviewRating": { 200 "@type": "Rating", 201 "ratingValue": 5, 202 "bestRating": 5, 203 "worstRating": 1 204 }, 205 "author": { 206 "@type": "Person", 207 "name": "Taufan â Surabaya" 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": "BERLINTOTO", 215 "reviewBody": "Urusan transaksi finansial di sini jempolan banget. Deposit pakai scan QRIS instan masuk hitungan detik tanpa perlu konfirmasi manual yang berbelit-belit. Withdraw berapapun selalu diproses cepat dan transparan tanpa ada biaya potongan tersembunyi.", 216 "reviewRating": { 217 "@type": "Rating", 218 "ratingValue": 5, 219 "bestRating": 5, 220 "worstRating": 1 221 }, 222 "author": { 223 "@type": "Person", 224 "name": "Thomas Tampi â Semarang" 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": "BERLINTOTO", 232 "reviewBody": "BERLINTOTO memiliki tampilan yang mudah dipahami untuk membuka situs toto slot dan situs toto togel. Saya dapat menemukan menu yang dibutuhkan dan mengakses halaman melalui perangkat yang digunakan.", 233 "reviewRating": { 234 "@type": "Rating", 235 "ratingValue": 5, 236 "bestRating": 5, 237 "worstRating": 1 238 }, 239 "author": { 240 "@type": "Person", 241 "name": "Soesilowati â Makassar" 242 } 243 } 244 ] 245} 246</script>
vendor: 1 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 itu BERLINTOTO sebagai portal utama situs toto slot gacor dan situs toto togel 2026?", 255 "acceptedAnswer": { 256 "@type": "Answer", 257 "text": "Hadir situs Toto slot gacor & bandar togel online 4D resmi, BERLINTOTO menyajikan ekosistem permainan digital terlengkap yang didukung oleh teknologi server modern, stabilitas akses tanpa kendala pemblokiran, serta jaminan transparansi permainan secara menyeluruh." 258 } 259}, 260{ 261 "@type": "Question", 262 "name": "Pasaran togel apa saja yang dapat dimainkan di BERLINTOTO?", 263 "acceptedAnswer": { 264 "@type": "Answer", 265 "text": "BERLINTOTO menyediakan pilihan pasaran togel online 4D resmi terpopuler dan terlengkap. Seluruh hasil pengeluaran angka diputar secara live draw dan diperbarui secara real-time langsung dari server pusat yang akurat." 266 } 267}, 268{ 269 "@type": "Question", 270 "name": "Bagaimana cara menggunakan BERLINTOTO untuk membuka situs toto slot dan situs toto togel?", 271 "acceptedAnswer": { 272 "@type": "Answer", 273 "text": "Keamanan member adalah prioritas utama. Seluruh infrastruktur jaringan BERLINTOTO dilindungi oleh sistem enkripsi data canggih yang menjaga kerahasiaan informasi pribadi serta menjamin keamanan penuh pada setiap riwayat transaksi finansial." 274 } 275}, 276{ 277 "@type": "Question", 278 "name": "Metode pembayaran apa saja yang disediakan untuk transaksi deposit dan withdraw?", 279 "acceptedAnswer": { 280 "@type": "Answer", 281 "text": "Proses transaksi diproses secara serba otomatis dan fleksibel. Member dapat menggunakan transfer bank lokal utama, fitur scan QRIS instan, hingga berbagai opsi e-wallet populer untuk melakukan pengisian saldo maupun penarikan dana." 282 } 283}, 284{ 285 "@type": "Question", 286 "name": "Ke mana harus menghubungi jika membutuhkan bantuan transaksi atau kendala akun?", 287 "acceptedAnswer": { 288 "@type": "Answer", 289 "text": "Layanan Customer Service profesional BERLINTOTO bersiaga penuh selama 24 jam nonstop setiap hari melalui fitur Live Chat dan kontak resmi untuk membantu kendala Anda dengan cepat dan ramah." 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="https://www.bim.miami/iconberlintoto.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:#FFF5E6; 314 --brand-100:#FFE7CC; 315 --brand-500:#F28C1A; 316 --brand-600:#D97809; 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,245,230,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 orange */ 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,245,230,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,245,230,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:#fb923c; 466 box-shadow:0 0 0 3px rgba(251, 146, 60, 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,245,230,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,245,230,1) 0%, 643 rgba(255,245,230,0.98) 15%, 644 rgba(255,245,230,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, #FFE7CC, #FFD9A8); 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:#fb923c; 889 box-shadow:0 0 0 3px rgba(251, 146, 60, 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:#fb923c; 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, #dcce77); 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,245,230,0.8), rgba(255,231,204,0.6)); 1058 border:1px solid rgba(242,140,26,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(242,140,26,0.08), 1070 0 0 0 1px rgba(255,255,255,0.5) inset, 1071 0 0 12px -6px rgba(242,140,26,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(242,140,26,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, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 1096 color:#D97809; 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(242,140,26,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, #F28C1A, #FFD9A8, #F28C1A); 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(242,140,26,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,245,230,0.95), rgba(255,231,204,0.85)); 1151 border-color:rgba(242,140,26,0.3); 1152 box-shadow: 1153 0 4px 16px -4px rgba(242,140,26,0.25), 1154 0 0 0 1px rgba(242,140,26,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,#FFFDFB 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(242,140,26,.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(242,140,26,.35); 1363} 1364 1365.usp-item:hover::before { 1366 background:radial-gradient(circle at 70% 40%, rgba(242,140,26,.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,#FFE7CC,#FFD9A8); 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:#F28C1A; 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:#F28C1A; 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-orange:#F28C1A; 1796 --ft-orange-dark:#D97809; 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(242,140,26,0.18); 1802 --ft-glow-fade:rgba(242,140,26,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-orange); 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(242,140,26,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(242,140,26,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 ORANGE ========== */ 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-orange-dark),var(--ft-orange)); 1861 box-shadow:0 0 12px -2px var(--ft-orange); 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(242,140,26,0.22) 0%, rgba(242,140,26,0.04) 38%, rgba(242,140,26,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 oranye 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(242,140,26,0),rgba(242,140,26,.65),rgba(242,140,26,0)); 1906} 1907 1908/* ========== VARIANT E: GLASS PANEL ========== */ 1909.site-footer.footer-compact.variant-glass { 1910 background:linear-gradient(145deg,#051c29 0%, #041620 65%, #03121b 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(242,140,26,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-orange); 1940 color:#fff; 1941 box-shadow:0 0 0 3px rgba(242,140,26,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: #F28C1A; 2099} 2100 2101.bottom-nav-item.active { 2102 flex-direction: row; 2103 gap: 8px; 2104 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 2105 color: white; 2106 border-radius: 20px; 2107 padding: 8px 16px; 2108 box-shadow: 0 2px 8px rgba(242, 140, 26, 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, #F28C1A 0%, #D97809 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: #F28C1A; 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, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 2291 border-radius: 50%; 2292 display: flex; 2293 align-items: center; 2294 justify-content: center; 2295 font-size: 24px; 2296 color: #D97809; 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: #fb923c; 2420 animation: botPulse 2s ease-in-out infinite; 2421} 2422 2423.ai-bot-toggle.active { 2424 background: rgba(251, 146, 60, 0.1); 2425} 2426 2427.ai-bot-toggle.active:hover .bot-icon { 2428 color: #f97316; 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, #f0f9ff 0%, #e0f2fe 100%); 2505 border-bottom: 1px solid #bae6fd; 2506 color: #0369a1; 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, #667eea 0%, #764ba2 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: #667eea; 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: #667eea; 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 #fbbf24; 2733 color: #92400e; 2734 font-size: 12px; 2735 font-weight: 500; 2736 text-align: center; 2737 cursor: default; 2738} 2739 2740.suggest-footer-hint svg { 2741 color: #f59e0b; 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 #f59e0b; 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: #ea580c; 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: #c2410c; 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, #fff5f0 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, #fb923c 0%, #f97316 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, 146, 60, 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 #fb923c; 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: #fb923c; 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, #fb923c 0%, #f97316 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, 146, 60, 0.4); 3015} 3016 3017.ai-search-trigger-btn:hover { 3018 transform: translateY(-2px); 3019 box-shadow: 0 6px 20px rgba(251, 146, 60, 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 #fb923c; 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, #fb923c 0%, #f97316 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, 146, 60, 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, #fff7ed 0%, #ffedd5 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, #fb923c 0%, #f97316 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: #f97316; 3181 background: #fff7ed; 3182 padding: 3px 10px; 3183 border-radius: 12px; 3184 white-space: nowrap; 3185 font-weight: 500; 3186 border: 1px solid #fed7aa; 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, #fbbf24 0%, #f59e0b 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: #f59e0b; 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: #dcce77; 3274 --pd-success-bg: #dcfce7; 3275 --pd-success-text: #166534; 3276 --pd-danger-bg: #fee2e2; 3277 --pd-danger-text: #dcce77; 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: #dcce77; 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 blue background */ 3924 .v-chip.selected { 3925 background: #eff6ff !important; 3926 border-color: #2563eb !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,#FF5722 0%,#764ba2 100%);color:#fff;font-size:.63rem;font-weight:600;letter-spacing:.6px;text-transform:uppercase;display:inline-block;line-height:1;vertical-align:middle;border:none} 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: #fff7ed; 4163 border-radius: 6px; 4164 font-size: .65rem; 4165 font-weight: 600; 4166 color: #ea580c; 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: #ffedd5; 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: #f59e0b; 4183 line-height: 1; 4184} 4185 4186.rating-inline .rating-value { 4187 color: #ea580c; 4188} 4189 4190.rating-inline .rating-badge-count { 4191 color: #9a3412; 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, 146, 60, 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:#2563eb;background:#eff6ff} 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:#fb923c;border:2px solid #fb923c;padding:0;width:44px;min-width:44px} 4254.btn-chat-icon:hover{background:rgba(251, 146, 60, 0.06);border-color:#f97316} 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, #7c3aed 0%, #9333ea 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:#dcce77;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:#fb923c; 4483 color:#fff; 4484 border:0; 4485 box-shadow:0 2px 8px rgba(251, 146, 60, 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:#fb923c; 4510 border:2px solid #fb923c; 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:#fb923c;border:2px solid #fb923c;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:#fb923c; 4525 border:2px solid #fb923c; 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:#0ea5e9; /* sky-500 */ 4736 --cd-bg2:#6366f1; /* 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:#f97316; /* orange-500 */ 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:#dcce77; 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%, #f57c00 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, #667eea 0%, #764ba2 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, #667eea 0%, #764ba2 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: #667eea; 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, #667eea15 0%, #764ba215 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: #3b82f6; 5822 color: #fff; 5823} 5824 5825.benchmark-score-badge.average { 5826 background: #f59e0b; 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: #667eea; 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,#f59e0b 0%,#ffb53a 35%,#1d6ce0 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,#1d6ce0 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:#1d6ce0; 6038 box-shadow:0 0 0 3px rgba(29,108,224,.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:#1d6ce0; 6059 box-shadow:0 0 0 2px rgba(29,108,224,.15),0 2px 8px -2px rgba(29,108,224,.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,#1d6ce0 0%,#155ab8 100%); 6110 border-color:#1d6ce0; 6111 color:#ffffff; 6112 box-shadow:0 2px 6px -1px rgba(29,108,224,.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(29,108,224,.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,#1d6ce0 0%,#155ab8 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(29,108,224,.45); 6205 transition:transform .25s, box-shadow .3s; 6206} 6207.f-actions button:hover { transform:translateY(-3px); box-shadow:0 8px 26px -4px rgba(29,108,224,.6); } 6208.f-actions button:active { transform:translateY(-1px) scale(.98); box-shadow:0 4px 14px -4px rgba(29,108,224,.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,#1d6ce0 0%,#155ab8 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(29,108,224,.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(29,108,224,.6); transform:translateY(-2px); } 6252.filter-toggle:active { transform:translateY(0); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6253.filter-toggle .count-badge { 6254 display:inline-block; 6255 background:linear-gradient(135deg,#f59e0b 0%,#ffb43d 55%,#f59e0b 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 #1d6ce0; 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,#dcce77 0%,#dcce77 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,#1d6ce0,#155ab8,#1d6ce0); 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(29,108,224,.4) !important; 6409 border-color:#1d6ce0 !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: #6366f1; 6430 --mm2-accent-hover: #4f46e5; 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: #4f46e5; 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);
6827 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: #dcce77; 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: #3b82f6; 6999 color: #3b82f6; 7000 background: #f0f9ff; 7001 transform: translateY(-1px); 7002 box-shadow: 0 2px 6px rgba(59, 130, 246, 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: #dcce77 !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: #dcce77 !important; 7052 color: #dcce77 !important; 7053 background: #fef2f2 !important; 7054} 7055 7056.pd-compare-btn.added .compare-remove svg {
7057 color: #dcce77 !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, #3b82f6 0%, #2563eb 100%); 7120 color: #fff; 7121 box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 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(59, 130, 246, 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: #dcce77; 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 #dcce77; 7425 background: transparent; 7426 color: #dcce77; 7427} 7428 7429.btn-outline-danger:hover { 7430 background: #dcce77; 7431 color: white; 7432} 7433 7434.btn-primary { 7435 border: 1px solid #3b82f6; 7436 background: #3b82f6; 7437 color: white; 7438} 7439 7440.btn-primary:hover { 7441 background: #2563eb; 7442 border-color: #2563eb; 7443} 7444 7445.btn-primary:disabled { 7446 opacity: 0.5;
7447 cursor: not-allowed; 7448} 7449 7450.btn-primary:disabled:hover { 7451 background: #3b82f6; 7452 border-color: #3b82f6; 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: #3b82f6; 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: #dcce77; 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: #fbbf24; 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: #dcce77; 7780} 7781 7782.analysis-section h6.best-for { 7783 color: #7c3aed; 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: #3b82f6 !important; 8168 border-color: #3b82f6 !important; 8169 color: white !important; 8170 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 8171 } 8172 8173 #compareAnalyze:not(:disabled) { 8174 background: #2563eb !important; 8175 border-color: #2563eb !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 #fbbf24; 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: #92400e; 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: #f0f9ff; 8220 border: 1px solid #0ea5e9; 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: #0ea5e9; 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(59, 130, 246, 0.7); 8336} 8337 8338@keyframes pulse-glow { 8339 0% { 8340 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8341 } 8342 50% { 8343 box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); 8344 } 8345 100% { 8346 box-shadow: 0 0 0 0 rgba(59, 130, 246, 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: #0ea5e9; 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: #ea580c; 8542} 8543 8544.pca-product-stock.out { 8545 color: #dcce77; 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: #0ea5e9; 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: #8b5cf6; 8625} 8626 8627/* Action Item Colors */ 8628.pca-action-item.cart .pca-action-icon { 8629 color: #0ea5e9; 8630} 8631 8632.pca-action-item.compare .pca-action-icon { 8633 color: #8b5cf6; 8634} 8635 8636.pca-action-item.compare.active { 8637 background: rgba(139, 92, 246, 0.05); 8638} 8639 8640.pca-action-item.compare.active .pca-action-label { 8641 color: #8b5cf6; 8642} 8643 8644.pca-action-item.compare.active .pca-action-icon { 8645 color: #8b5cf6; 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: #dcce77; 8855} 8856 8857.modal-btn-primary { 8858 background: #3b82f6; 8859 color: white; 8860} 8861 8862.modal-btn-primary:hover { 8863 background: #2563eb; 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: #f59e0b; 8884} 8885 8886.modal-icon.success { 8887 background: #f0fdf4; 8888 color: #22c55e; 8889} 8890 8891.modal-icon.info { 8892 background: #eff6ff; 8893 color: #3b82f6; 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: #2563eb; 8957 border-bottom-color: #2563eb; 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: #fbbf24; 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, #fbbf24 0%, #f59e0b 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: #2563eb; 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: #1d4ed8; 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: #fbbf24; 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: #dbeafe; 9199 color: #1d4ed8; 9200 border-color: #93c5fd; 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, #667eea 0%, #764ba2 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 #fbbf24; 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: #dcce77; 9481} 9482 9483.review-toast.toast-info { 9484 background: #2563eb; 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: #dcce77; 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, #3b82f6 0%, #2563eb 100%); 9781 border-radius: 4px; 9782} 9783 9784.review-modal .modal-body::-webkit-scrollbar-thumb:hover { 9785 background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%); 9786} 9787 9788/* Firefox Scrollbar */ 9789.review-modal .modal-body { 9790 scrollbar-width: thin; 9791 scrollbar-color: #3b82f6 #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: #fbbf24; 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: #2563eb; 9892 box-shadow: 0 0 0 3px rgba(37, 99, 235, 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: #2563eb; 9920 color: #2563eb; 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 #fbbf24; 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: #92400e; 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(251, 191, 36, 0.3); 10000 font-size: 16px; 10001 color: #92400e; 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: #2563eb; 10028 color: white; 10029} 10030 10031.review-form .btn-primary:hover { 10032 background: #1d4ed8; 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 #f59e0b; 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: #f59e0b; 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: #fcd34d; 10233} 10234 10235.point-summary-card.psc-pending-card .psc-value { 10236 color: #b45309; 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: #fcd34d; 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: #92400e; 10264 border: 1px solid #fcd34d; 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 Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi 10306 </div> 10307 <div class="topbar-right"> 10308 <a href="https://www.bim.miami/">TOGEL</a> 10309 <a href="https://www.bim.miami/">BANDAR TOGEL</a> 10310 <a href="https://www.bim.miami/">BERLINTOTO</a> 10311 <a href="https://www.bim.miami/">BANDAR BERLINTOTO</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://www.bim.miami/" class="logo-mobile" title="BERLINTOTO"> 10319 <img src="https://www.bim.miami/logoberlintoto.png" alt="BERLINTOTO" height="40"> 10320 </a> 10321 </div> 10322</nav> 10323 <div class="container nav-inner"> 10324 <a href="https://www.bim.miami/" class="logo" title="BERLINTOTO"> 10325 <img src="https://www.bim.miami/logoberlintoto.png" alt="BERLINTOTO" 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="Apa Carik Palku?" 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://www.bim.miami/"> 10370 BERLINTOTO </a> 10371 <a class="" href="https://www.bim.miami/"> 10372 BERLIN TOTO </a> 10373 <a class="" href="https://www.bim.miami/"> 10374 LOGIN BERLINTOTO </a> 10375 <a class="" href="https://www.bim.miami/"> 10376 DAFTAR BERLINTOTO </a> 10377 <a class="" href="https://www.bim.miami/"> 10378 LINK BERLINTOTO </a> 10379 <a class="" href="https://www.bim.miami/"> 10380 SLOT GACOR </a> 10381 </div> 10382 <div class="mega-cats-fade" id="megaCatsFade"></div> 10383 </div> 10384 </div> 10385</nav> 10386 10387<!-- Panel Mega Menu - Outside navbar for full-screen --> 10388<div class="mm2-panel" data-mm2="panel" role="dialog" aria-label="Kategori"> 10389 <div class="mm2-view is-active" data-mm2="top"></div> 10390 <div class="mm2-view" data-mm2="detail"></div> 10391</div> 10392 10393<!-- Backdrop - Outside navbar for full-screen --> 10394<div class="mm2-backdrop" data-mm2="backdrop"></div> 10395 10396<!-- Mobile Bottom Navigation --> 10397<nav class="bottom-nav"> 10398 <div class="bottom-nav-inner"> 10399 <!-- Home --> 10400 <a href="/" class="bottom-nav-item" id="bottomHomeBtn" data-page="home"> 10401 <div class="bottom-nav-item-icon"> 10402 <!-- Outlined icon (default) --> 10403 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10404 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 10405 <polyline points="9 22 9 12 15 12 15 22"/> 10406 </svg> 10407 <!-- Filled icon (active) --> 10408 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10409 <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"/> 10410 <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"/> 10411 </svg> 10412 </div> 10413 <span class="bottom-nav-item-label">Home</span> 10414 </a> 10415 10416 <!-- Kategori --> 10417 <button class="bottom-nav-item" id="bottomCategoryBtn" data-mm2="trigger" aria-label="Kategori"> 10418 <div class="bottom-nav-item-icon"> 10419 <!-- Outlined icon (default) --> 10420 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10421 <line x1="3" y1="12" x2="21" y2="12"/> 10422 <line x1="3" y1="6" x2="21" y2="6"/> 10423 <line x1="3" y1="18" x2="21" y2="18"/> 10424 </svg> 10425 <!-- Filled icon (active) --> 10426 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10427 <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"/> 10428 </svg> 10429 </div> 10430 <span class="bottom-nav-item-label">Kategori</span> 10431 </button> 10432 10433 <!-- Keranjang --> 10434 <button class="bottom-nav-item" id="bottomCartBtn"> 10435 <div class="bottom-nav-item-icon"> 10436 <!-- Outlined icon (default) --> 10437 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10438 <circle cx="9" cy="21" r="1"/> 10439 <circle cx="20" cy="21" r="1"/> 10440 <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/> 10441 </svg> 10442 <!-- Filled icon (active) -->
10443 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10444 <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"/> 10445 </svg> 10446 <span class="bottom-nav-item-badge" id="bottomCartCount">0</span> 10447 </div> 10448 <span class="bottom-nav-item-label">Keranjang</span> 10449 </button> 10450 10451 <!-- Compare --> 10452 <button class="bottom-nav-item" id="bottomCompareBtn"> 10453 <div class="bottom-nav-item-icon"> 10454 <!-- Outlined icon (default) --> 10455 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10456 <polyline points="17 11 21 7 17 3"/> 10457 <path d="M21 7H9"/> 10458 <polyline points="7 21 3 17 7 13"/> 10459 <path d="M15 17H3"/> 10460 </svg> 10461 <!-- Filled icon (active) --> 10462 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10463 <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"/> 10464 </svg> 10465 <span class="bottom-nav-item-badge" id="bottomCompareCount" style="display:none;">0</span> 10466 </div> 10467 <span class="bottom-nav-item-label">Compare</span> 10468 </button> 10469 10470 <!-- Chat - Always show, check login in JavaScript --> 10471 <button class="bottom-nav-item" id="bottomChatBtn" data-page="chat"> 10472 <div class="bottom-nav-item-icon"> 10473 <!-- Outlined icon (default) --> 10474 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" opacity="0.85"> 10475 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 10476 </svg> 10477 <!-- Filled icon (active) --> 10478 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10479 <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/> 10480 </svg> 10481 <span class="bottom-nav-item-badge" id="bottomChatCount" style="display:none;">0</span> 10482 </div> 10483 <span class="bottom-nav-item-label">Chat</span> 10484 </button> 10485 10486 <!-- Akun --> 10487 <button class="bottom-nav-item" id="bottomLoginBtn"> 10488 <div class="bottom-nav-item-icon"> 10489 <!-- Outlined icon (default) --> 10490 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10491 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 10492 <circle cx="12" cy="7" r="4"/> 10493 </svg> 10494 <!-- Filled icon (active) --> 10495 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10496 <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"/> 10497 </svg> 10498 </div> 10499 <span class="bottom-nav-item-label">Login</span> 10500 </button> 10501 </div> 10502</nav> 10503 10504<!-- Mobile User Menu Bottom Sheet --> 10505
10506<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 10507(function(){ 10508 const btn = document.getElementById('megaMoreBtn'); 10509 const dd = document.getElementById('megaMoreDropdown'); 10510 if(!btn || !dd) return; 10511 10512 function openDD(){ 10513 dd.hidden = false; 10514 btn.setAttribute('aria-expanded','true'); 10515 document.addEventListener('click', outside, {capture:true}); 10516 document.addEventListener('keydown', esc); 10517 } 10518 function closeDD(){ 10519 if(dd.hidden) return; 10520 dd.hidden = true; 10521 btn.setAttribute('aria-expanded','false'); 10522 document.removeEventListener('click', outside, {capture:true}); 10523 document.removeEventListener('keydown', esc); 10524 } 10525 function toggle(){ dd.hidden ? openDD() : closeDD(); } 10526 function outside(e){ 10527 if(!dd.contains(e.target) && e.target !== btn){ 10528 closeDD(); 10529 } 10530 } 10531 function esc(e){ 10532 if(e.key === 'Escape'){ 10533 closeDD(); 10534 btn.focus(); 10535 } 10536 } 10537 btn.addEventListener('click', toggle); 10538})(); 10539 10540// ========== HORIZONTAL SCROLL FADE INDICATOR ========== 10541(function(){ 10542 const megaCats = document.getElementById('megaCatsScroll'); 10543 const fade = document.getElementById('megaCatsFade'); 10544 if (!megaCats || !fade) return; 10545 10546 function updateFade() { 10547 const scrollLeft = megaCats.scrollLeft; 10548 const scrollWidth = megaCats.scrollWidth; 10549 const clientWidth = megaCats.clientWidth; 10550 const maxScroll = scrollWidth - clientWidth; 10551 10552 // Hide fade when scrolled to end (within 10px tolerance) 10553 if (maxScroll - scrollLeft <= 10) { 10554 fade.style.opacity = '0'; 10555 } else { 10556 fade.style.opacity = '1'; 10557 } 10558 } 10559 10560 // Update on scroll 10561 megaCats.addEventListener('scroll', updateFade, { passive: true }); 10562 10563 // Update on resize 10564 window.addEventListener('resize', updateFade, { passive: true }); 10565 10566 // Initial check 10567 setTimeout(updateFade, 100); 10568})(); 10569 10570document.addEventListener('DOMContentLoaded', function () { 10571 var input = document.getElementById('searchInput'); 10572 if (!input) return; 10573 input.addEventListener('keydown', function (e) { 10574 if (e.key === 'Enter') { 10575 e.preventDefault(); 10576 var form = input.form; 10577 if (!form) return; 10578 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 10579 else form.submit(); 10580 } 10581 }); 10582}); 10583 10584 10585(function(){ 10586 const cartBtn = document.getElementById('cartBtn'); 10587 if (cartBtn) { 10588 cartBtn.addEventListener('click', function(e) { 10589 e.preventDefault(); 10590 10591 const checkLogin = () => { 10592 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10593 }; 10594 10595 if (checkLogin()) { 10596 window.location.href = '/keranjang'; 10597 } else { 10598 showCartLoginModal(); 10599 } 10600 }); 10601 } 10602 10603 const loginBtn = document.getElementById('loginBtn'); 10604 if (loginBtn) { 10605 loginBtn.addEventListener('click', function(e) { 10606 e.preventDefault(); 10607 window.location.href = 'https://www.bim.miami/'; 10608 }); 10609 } 10610 10611 const userMenuBtn = document.getElementById('userMenuBtn'); 10612 const userDropdown = document.getElementById('userDropdown'); 10613 10614 if (userMenuBtn && userDropdown) { 10615 function openDropdown() { 10616 userDropdown.hidden = false; 10617 userMenuBtn.setAttribute('aria-expanded', 'true'); 10618 document.addEventListener('click', closeOnOutside, { capture: true }); 10619 document.addEventListener('keydown', closeOnEscape); 10620 } 10621 10622 function closeDropdown() { 10623 if (userDropdown.hidden) return; 10624 userDropdown.hidden = true; 10625 userMenuBtn.setAttribute('aria-expanded', 'false'); 10626 document.removeEventListener('click', closeOnOutside, { capture: true }); 10627 document.removeEventListener('keydown', closeOnEscape); 10628 } 10629 10630 function closeOnOutside(e) { 10631 if (!userDropdown.contains(e.target) && e.target !== userMenuBtn) { 10632 closeDropdown(); 10633 } 10634 } 10635 10636 function closeOnEscape(e) { 10637 if (e.key === 'Escape') { 10638 closeDropdown(); 10639 userMenuBtn.focus(); 10640 } 10641 } 10642 10643 userMenuBtn.addEventListener('click', function(e) { 10644 e.preventDefault(); 10645 userDropdown.hidden ? openDropdown() : closeDropdown(); 10646 }); 10647 10648 // Close dropdown when clicking profile links 10649 userDropdown.querySelectorAll('[data-close-dropdown]').forEach(link => { 10650 link.addEventListener('click', function() { 10651 closeDropdown(); 10652 }); 10653 }); 10654 } 10655 10656 // Bottom nav profile button - toggle mobile bottom sheet 10657 const bottomProfileBtn = document.getElementById('bottomProfileBtn'); 10658 const mobileUserSheet = document.getElementById('mobileUserSheet'); 10659 const mobileUserSheetBackdrop = document.getElementById('mobileUserSheetBackdrop'); 10660 10661 if (bottomProfileBtn && mobileUserSheet) { 10662 function openMobileSheet() { 10663 mobileUserSheet.hidden = false;
10664 document.body.style.overflow = 'hidden'; 10665 // Trigger animation 10666 requestAnimationFrame(() => { 10667 mobileUserSheet.classList.add('active'); 10668 }); 10669 } 10670 10671 function closeMobileSheet() { 10672 mobileUserSheet.classList.remove('active'); 10673 document.body.style.overflow = ''; 10674 setTimeout(() => { 10675 mobileUserSheet.hidden = true; 10676 }, 300); 10677 } 10678 10679 bottomProfileBtn.addEventListener('click', function(e) { 10680 e.preventDefault(); 10681 openMobileSheet(); 10682 }); 10683 10684 if (mobileUserSheetBackdrop) { 10685 mobileUserSheetBackdrop.addEventListener('click', closeMobileSheet); 10686 } 10687 10688 // Close on escape key 10689 document.addEventListener('keydown', function(e) { 10690 if (e.key === 'Escape' && !mobileUserSheet.hidden) { 10691 closeMobileSheet(); 10692 } 10693 }); 10694 10695 // Close sheet when clicking profile links 10696 mobileUserSheet.querySelectorAll('[data-close-sheet]').forEach(link => { 10697 link.addEventListener('click', function() { 10698 closeMobileSheet(); 10699 }); 10700 }); 10701 } 10702 10703 // Handle Google avatar error in bottom-nav (fallback to initials) 10704 const bottomNavAvatar = document.querySelector('.bottom-nav-avatar-img'); 10705 if (bottomNavAvatar) { 10706 bottomNavAvatar.addEventListener('error', function() { 10707 const initials = this.getAttribute('data-fallback-initials') || 'U'; 10708 const initialsDiv = document.createElement('div'); 10709 initialsDiv.className = 'bottom-nav-avatar-initials'; 10710 initialsDiv.textContent = initials; 10711 this.parentNode.replaceChild(initialsDiv, this); 10712 }); 10713 } 10714 10715 // Mobile logout button 10716 const mobileLogoutBtn = document.getElementById('mobileLogoutBtn'); 10717 if (mobileLogoutBtn) { 10718 mobileLogoutBtn.addEventListener('click', async function(e) { 10719 e.preventDefault(); 10720 10721 // Check push notification before logout 10722 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10723 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10724 if (!confirmLogout) { 10725 return; // User cancelled logout 10726 } 10727 } 10728 10729 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10730 10731 try { 10732 const response = await fetch('/auth/logout', { 10733 method: 'POST', 10734 credentials: 'same-origin', 10735 headers: { 10736 'Content-Type': 'application/json', 10737 'X-CSRF-Token': getCsrfToken() 10738 }, 10739 body: JSON.stringify({ 10740 csrf_token: getCsrfToken() 10741 }) 10742 }); 10743 10744 if (response.ok) { 10745 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10746 window.guestCart.clear(); 10747 } 10748 window.location.href = '/login?logged_out=1'; 10749 } else { 10750 console.error('Logout failed'); 10751 window.location.href = '/logout'; 10752 } 10753 } catch (error) { 10754 console.error('Logout error:', error); 10755 window.location.href = '/logout'; 10756 } 10757 }); 10758 } 10759 10760 const logoutBtn = document.getElementById('logoutBtn'); 10761 if (logoutBtn) { 10762 logoutBtn.addEventListener('click', async function(e) { 10763 e.preventDefault(); 10764 10765 // Check push notification before logout 10766 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10767 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10768 if (!confirmLogout) { 10769 return; // User cancelled logout 10770 } 10771 } 10772 10773 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10774 10775 try { 10776 const response = await fetch('/auth/logout', { 10777 method: 'POST', 10778 credentials: 'same-origin', 10779 headers: { 10780 'Content-Type': 'application/json', 10781 'X-CSRF-Token': getCsrfToken() 10782 }, 10783 body: JSON.stringify({ 10784 csrf_token: getCsrfToken() 10785 }) 10786 }); 10787 10788 if (response.ok) { 10789 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10790 window.guestCart.clear(); 10791 } 10792 10793 window.location.href = '/login?logged_out=1'; 10794 } else { 10795 console.error('Logout failed'); 10796 window.location.href = '/logout'; 10797 } 10798 } catch (error) { 10799 console.error('Logout error:', error); 10800 window.location.href = '/logout'; 10801 } 10802 }); 10803 } 10804 10805 const wishlistBtn = document.getElementById('wishlistBtn'); 10806 const notifBtn = document.getElementById('notifBtn'); 10807 10808 if (wishlistBtn) { 10809 wishlistBtn.addEventListener('click', function(e) { 10810 e.preventDefault(); 10811 alert('Fitur wishlist akan segera tersedia!'); 10812 }); 10813 } 10814 10815 if (notifBtn) { 10816 notifBtn.addEventListener('click', function(e) { 10817 e.preventDefault(); 10818 alert('Fitur notifikasi akan segera tersedia!'); 10819 }); 10820 } 10821})(); 10822 10823// ========== MOBILE BOTTOM NAVIGATION ========== 10824(function initBottomNav() { 10825 // Sync cart badge with bottom nav 10826 function syncBottomCartBadge() { 10827 const mainBadge = document.getElementById('cartCount'); 10828 const bottomBadge = document.getElementById('bottomCartCount'); 10829 if (mainBadge && bottomBadge) { 10830 const count = mainBadge.textContent; 10831 bottomBadge.textContent = count; 10832 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10833 } 10834 } 10835 10836 // Sync compare badge with bottom nav 10837 function syncBottomCompareBadge() { 10838 const mainBadge = document.getElementById('compareCount'); 10839 const bottomBadge = document.getElementById('bottomCompareCount'); 10840 if (mainBadge && bottomBadge) { 10841 const count = mainBadge.textContent; 10842 bottomBadge.textContent = count; 10843 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10844 } 10845 } 10846 10847 // Initial sync 10848 syncBottomCartBadge(); 10849 syncBottomCompareBadge(); 10850 10851 // Observe badge changes 10852 const observer = new MutationObserver(function() { 10853 syncBottomCartBadge(); 10854 syncBottomCompareBadge(); 10855 }); 10856 10857 const cartBadge = document.getElementById('cartCount'); 10858 const compareBadge = document.getElementById('compareCount'); 10859 10860 if (cartBadge) observer.observe(cartBadge, { childList: true, characterData: true, subtree: true }); 10861 if (compareBadge) observer.observe(compareBadge, { childList: true, characterData: true, subtree: true }); 10862 10863 // Bottom Cart Button 10864 const bottomCartBtn = document.getElementById('bottomCartBtn'); 10865 if (bottomCartBtn) { 10866 bottomCartBtn.addEventListener('click', function(e) { 10867 e.preventDefault(); 10868 10869 const checkLogin = () => { 10870 return window.isUserLoggedIn ? window.isUserLoggedIn() : false;
10871 }; 10872 10873 if (checkLogin()) { 10874 window.location.href = '/keranjang'; 10875 } else { 10876 showCartLoginModal(); 10877 } 10878 }); 10879 } 10880 10881 // Bottom Compare Button 10882 const bottomCompareBtn = document.getElementById('bottomCompareBtn'); 10883 if (bottomCompareBtn) { 10884 bottomCompareBtn.addEventListener('click', function(e) { 10885 e.preventDefault(); 10886 10887 // Close chat widget if open 10888 if (window.plazaChat && window.plazaChat.isWidgetOpen) { 10889 window.plazaChat.closeWidget(); 10890 } 10891 10892 const mainCompareBtn = document.getElementById('compareBtn'); 10893 if (mainCompareBtn) { 10894 mainCompareBtn.click(); 10895 } 10896 }); 10897 } 10898 10899 // Bottom Category Button - triggers mega menu 10900 const bottomCategoryBtn = document.getElementById('bottomCategoryBtn'); 10901 if (bottomCategoryBtn) { 10902 bottomCategoryBtn.addEventListener('click', function(e) { 10903 e.preventDefault(); 10904 // Trigger desktop mega menu 10905 const desktopTrigger = document.querySelector('.mm2-trigger:not(#bottomCategoryBtn)'); 10906 if (desktopTrigger) { 10907 desktopTrigger.click(); 10908 } 10909 }); 10910 } 10911 10912 // Bottom Login Button 10913 const bottomLoginBtn = document.getElementById('bottomLoginBtn'); 10914 if (bottomLoginBtn) { 10915 bottomLoginBtn.addEventListener('click', function(e) { 10916 e.preventDefault(); 10917 window.location.href = '/login'; 10918 }); 10919 } 10920 10921 // Bottom Chat Button 10922 const bottomChatBtn = document.getElementById('bottomChatBtn'); 10923 if (bottomChatBtn) { 10924 bottomChatBtn.addEventListener('click', function(e) { 10925 e.preventDefault(); 10926 10927 // Trigger chat widget (try multiple times if needed) 10928 const triggerChat = () => { 10929 const chatToggle = document.getElementById('chat-toggle-btn'); 10930 if (chatToggle) { 10931 chatToggle.click(); 10932 return true; 10933 } 10934 return false; 10935 }; 10936 10937 // Immediate attempt 10938 if (!triggerChat()) { 10939 // Retry after 100ms if not found 10940 setTimeout(() => { 10941 if (!triggerChat()) { 10942 // Last resort: manually show popup 10943 const popup = document.getElementById('chat-popup'); 10944 if (popup) { 10945 popup.style.display = 'block'; 10946 } 10947 } 10948 }, 100); 10949 } 10950 }); 10951 } 10952 10953 // Sync chat unread badge 10954 function syncBottomChatBadge() { 10955 const chatBadge = document.getElementById('chat-unread-badge'); 10956 const bottomChatBadge = document.getElementById('bottomChatCount'); 10957 if (chatBadge && bottomChatBadge) { 10958 const count = chatBadge.textContent; 10959 bottomChatBadge.textContent = count; 10960 bottomChatBadge.style.display = chatBadge.style.display; 10961 } 10962 } 10963 10964 // Observe chat badge changes 10965 const chatBadge = document.getElementById('chat-unread-badge'); 10966 if (chatBadge) { 10967 observer.observe(chatBadge, { attributes: true, childList: true, characterData: true, subtree: true }); 10968 syncBottomChatBadge(); 10969 } 10970 10971 // Active state management based on current page 10972 function updateActiveState() { 10973 const currentPath = window.location.pathname; 10974 const bottomNavItems = document.querySelectorAll('.bottom-nav-item'); 10975 10976 bottomNavItems.forEach(item => { 10977 item.classList.remove('active'); 10978 10979 const itemId = item.id; 10980 const page = item.getAttribute('data-page'); 10981 const href = item.getAttribute('href'); 10982 10983 // Check by ID for specific pages 10984 if (itemId === 'bottomHomeBtn' && currentPath === '/') { 10985 item.classList.add('active'); 10986 } else if (itemId === 'bottomCartBtn' && (currentPath === '/keranjang' || currentPath === '/cart')) { 10987 item.classList.add('active'); 10988 } else if (itemId === 'bottomProfileBtn' && currentPath.startsWith('/profile')) { 10989 item.classList.add('active'); 10990 } else if (itemId === 'bottomCategoryBtn' && currentPath.startsWith('/kategori')) { 10991 item.classList.add('active'); 10992 } else if (itemId === 'bottomCompareBtn' && currentPath === '/compare') { 10993 item.classList.add('active'); 10994 } else if (itemId === 'bottomChatBtn' && currentPath === '/chat') { 10995 item.classList.add('active'); 10996 } 10997 // Fallback: check by data-page attribute 10998 else if (page === 'home' && currentPath === '/') { 10999 item.classList.add('active'); 11000 } else if (page === 'profile' && currentPath.startsWith('/profile')) { 11001 item.classList.add('active'); 11002 } 11003 // Fallback: check by href 11004 else if (href && currentPath === href) { 11005 item.classList.add('active'); 11006 } 11007 }); 11008 } 11009 11010 // Update active state on load 11011 updateActiveState(); 11012 11013 // Bottom nav always visible - auto-hide disabled for better UX 11014 // const bottomNav = document.querySelector('.bottom-nav'); 11015 // if (bottomNav && window.innerWidth <= 640) { 11016 // // Auto-hide feature disabled 11017 // } 11018})(); 11019// Cart Login Modal Handler (Global scope) 11020function showCartLoginModal() { 11021 const modal = document.getElementById('cartLoginModal'); 11022 if (!modal) return; 11023 11024 modal.classList.add('show'); 11025 11026 const confirmBtn = document.getElementById('cartLoginConfirm'); 11027 const registerBtn = document.getElementById('cartRegisterBtn'); 11028 const cancelBtn = document.getElementById('cartLoginCancel'); 11029 11030 const handleLogin = () => { 11031 window.location.href = '/login?return=' + encodeURIComponent('/keranjang'); 11032 }; 11033 11034 const handleRegister = () => { 11035 window.location.href = '/register?return=' + encodeURIComponent('/keranjang'); 11036 }; 11037 11038 const handleCancel = () => { 11039 modal.classList.remove('show'); 11040 confirmBtn.removeEventListener('click', handleLogin); 11041 registerBtn.removeEventListener('click', handleRegister); 11042 cancelBtn.removeEventListener('click', handleCancel); 11043 }; 11044 11045 confirmBtn.removeEventListener('click', handleLogin); 11046 registerBtn.removeEventListener('click', handleRegister); 11047 cancelBtn.removeEventListener('click', handleCancel); 11048 11049 confirmBtn.addEventListener('click', handleLogin); 11050 registerBtn.addEventListener('click', handleRegister); 11051 cancelBtn.addEventListener('click', handleCancel); 11052 11053 modal.addEventListener('click', (e) => { 11054 if (e.target === modal) { 11055 handleCancel(); 11056 } 11057 }); 11058} 11059</script>
vendor: 1 bytes, line 11059
11059
11060<script data-inlined="cart-badge.js"> 11061(function(){ 11062 function selBadge(){ 11063 return document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 11064 } 11065 function setBadge(n){ 11066 const el = selBadge(); 11067 if (!el) return; 11068 const count = Math.max(0, parseInt(n||0,10) || 0); 11069 el.textContent = String(count); 11070 // Sembunyikan jika 0? Jika ingin selalu terlihat, hapus 2 baris di bawah 11071 if (count <= 0) { el.classList.add('is-empty'); } 11072 else { el.classList.remove('is-empty'); } 11073 } 11074 async function refresh(){ 11075 try{ 11076 // Primary: Use server-side endpoint (handles both logged and guest users) 11077 const res = await fetch('/api/cart/get-simple.php', { credentials:'same-origin' }); 11078 if (res.ok) { 11079 const data = await res.json(); 11080 if (data && data.ok) { 11081 setBadge(data.items_count || 0); 11082 return; 11083 } 11084 } 11085 11086 // Fallback hanya untuk guest user: check localStorage 11087 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 11088 document.body.classList.contains('logged-in') || 11089 window.userLoggedIn === true; 11090 11091 if (!isLoggedIn && window.guestCart && typeof window.guestCart.getItemsCount === 'function') { 11092 setBadge(window.guestCart.getItemsCount()); 11093 } 11094 }catch(e){ 11095 // Final fallback hanya untuk guest 11096 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 11097 if (!isLoggedIn) { 11098 try { 11099 const guestCartData = JSON.parse(localStorage.getItem('plazait_guest_cart') || '{"items":{}}'); 11100 const items = guestCartData.items || {}; 11101 const count = Object.values(items).reduce((total, item) => total + (item.qty || 0), 0); 11102 setBadge(count); 11103 } catch(e2) { 11104 setBadge(0); 11105 } 11106 } else { 11107 setBadge(0); 11108 } 11109 } 11110 } 11111 11112 // Ekspos untuk dipanggil dari skrip lain (opsional) 11113 window.CartBadge = { set: setBadge, refresh }; 11114 window.updateCartCounter = refresh; // Alias for compatibility 11115 11116 // Update saat halaman load 11117 if (document.readyState === 'loading') { 11118 document.addEventListener('DOMContentLoaded', refresh); 11119 } else { 11120 refresh(); 11121 } 11122 11123 // Dengarkan event global dari add/remove/update 11124 document.addEventListener('cart:updated', function(ev){ 11125 const c = ev && ev.detail && typeof ev.detail.items_count === 'number' 11126 ? ev.detail.items_count : null; 11127 if (c !== null) setBadge(c); 11128 else refresh(); 11129 }); 11130 11131 // Dengarkan event khusus cart remove untuk update badge 11132 document.addEventListener('cart:removed', function(ev){ 11133 // Langsung refresh untuk memastikan data terbaru 11134 refresh(); 11135 }); 11136 11137 // Jika addToCart sudah ada sebelum file ini, coba wrap agar badge ter-update. 11138 const prevAdd = window.addToCart; 11139 if (typeof prevAdd === 'function'){ 11140 window.addToCart = async function(pid, qty, vid, meta){ 11141 const res = await prevAdd(pid, qty, vid, meta); 11142 try{ 11143 const cnt = res && res.cart ? res.cart.items_count : null; 11144 if (cnt !== null) setBadge(cnt); 11145 // tetap siarkan event agar listener lain ikut sync 11146 document.dispatchEvent(new CustomEvent('cart:updated', { detail: res.cart || {} })); 11147 }catch(_){} 11148 return res; 11149 }; 11150 } 11151 11152 // Helper function to check if user is logged in 11153 function isUserLoggedIn() { 11154 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 11155 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 11156 } 11157 11158 // Make login check globally accessible 11159 window.isUserLoggedIn = isUserLoggedIn; 11160})(); 11161</script>
11161 11162 11163<!-- Product Compare Sidebar --> 11164<div class="compare-sidebar" id="compareSidebar"> 11165 <div class="compare-header"> 11166 <h4 class="compare-title"> 11167 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 11168 <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"/> 11169 </svg> 11170 Bandingkan Produk 11171 <span class="compare-count" id="compareCountSidebar">(0)</span> 11172 </h4> 11173 <button class="compare-close" id="compareClose" aria-label="Tutup"> 11174 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11175 <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"/> 11176 </svg> 11177 </button> 11178 </div> 11179 11180 <div class="compare-content"> 11181 <div class="compare-empty" id="compareEmpty"> 11182 <div class="empty-icon"> 11183 <svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" opacity="0.3"> 11184 <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"/> 11185 </svg> 11186 </div> 11187 <h5>Belum ada produk dipilih</h5> 11188 <p>Pilih produk yang ingin dibandingkan dengan klik tombol "Bandingkan Produk" di halaman produk.</p> 11189 </div> 11190 11191 <div class="compare-list" id="compareList"> 11192 <!-- Products will be populated here --> 11193 </div> 11194 </div> 11195 11196 <div class="compare-actions" id="compareActions"> 11197 <button class="btn btn-outline-danger btn-sm" id="compareClearAll"> 11198 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11199 <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"/> 11200 </svg> 11201 Hapus Semua 11202 </button> 11203 <button class="btn btn-primary btn-sm" id="compareAnalyze" disabled> 11204 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11205 <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"/> 11206 </svg> 11207 Analisis AI 11208 </button> 11209 </div> 11210 11211 <!-- AI Disclaimer in Sidebar --> 11212 <div class="compare-disclaimer"> 11213 <div class="ai-disclaimer"> 11214 <div class="disclaimer-text"> 11215 <strong>ð¡ Catatan:</strong> Analisis ini dihasilkan secara otomatis dan dapat memiliki keterbatasan. 11216 Gunakan sebagai referensi tambahan dengan tetap mempertimbangkan kebutuhan Anda. 11217 Untuk informasi lebih lanjut, silakan hubungi tim BERLINTOTO. 11218 </div> 11219 </div> 11220 </div> 11221</div><style data-inlined="pd-lightbox.css"> 11222/* PlazaIT Product Lightbox (vanilla, no CDN) â v4 */ 11223.pd-lb { 11224 position: fixed; inset: 0; z-index: 99999; 11225 background: rgba(0,0,0,.95); 11226 display: none; 11227 color: #fff; 11228 /* Transition untuk swipe-down feedback */ 11229 transition: transform .18s ease, opacity .18s ease; 11230} 11231.pd-lb.show { display: block; } 11232 11233/* Canvas di bawah (z:1), Controls di atas (z:2) */ 11234.pd-lb-canvas { 11235 position: absolute; inset: 56px 0 56px 0; 11236 display: flex; align-items: center; justify-content: center; 11237 overflow: hidden; 11238 touch-action: none; 11239 z-index: 1; 11240} 11241 11242.pd-lb-img { 11243 max-width: 96vw; max-height: 86vh; 11244 will-change: transform; 11245 user-select: none; 11246 pointer-events: none; 11247 transform-origin: 50% 50%; 11248} 11249 11250.pd-lb-close, .pd-lb-prev, .pd-lb-next, .pd-lb-counter { 11251 position: absolute; 11252 z-index: 2; 11253} 11254 11255.pd-lb-close, .pd-lb-prev, .pd-lb-next { 11256 background: rgba(17,24,39,.7); 11257 color: #fff; border: 0; cursor: pointer; 11258 width: 40px; height: 40px; border-radius: 999px; 11259 display: inline-flex; align-items: center; justify-content: center; 11260 transition: background-color .15s ease, transform .05s ease; 11261 -webkit-tap-highlight-color: transparent; 11262} 11263.pd-lb-close:hover, .pd-lb-prev:hover, .pd-lb-next:hover { background: rgba(31,41,55,.85); } 11264.pd-lb-close:active, .pd-lb-prev:active, .pd-lb-next:active { transform: translateY(.5px); } 11265 11266.pd-lb-close { top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); } 11267.pd-lb-prev { top: 50%; left: 10px; transform: translateY(-50%); } 11268.pd-lb-next { top: 50%; right: 10px; transform: translateY(-50%); } 11269 11270.pd-lb-counter { 11271 top: max(16px, env(safe-area-inset-top, 16px)); left: 50%; 11272 transform: translateX(-50%); 11273 font-weight: 700; font-size: 13px; letter-spacing: .4px; 11274 background: rgba(17,24,39,.65); 11275 padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.1); 11276} 11277 11278@media (max-width: 640px){ 11279 .pd-lb-img { max-width: 98vw; max-height: 80vh; } 11280 .pd-lb-prev, .pd-lb-next { width: 36px; height: 36px; } 11281 .pd-lb-close { width: 38px; height: 38px; } 11282} 11283</style> 11284<style>.paylater-promo{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#f0fdf4 0,#dcfce7 100%);border:1px solid #86efac;border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.4;margin:8px 0}.paylater-promo-sm{padding:6px 10px;font-size:11px;gap:6px}.paylater-promo-md{padding:8px 12px;font-size:13px;gap:8px}.paylater-promo-lg{padding:10px 16px;font-size:14px;gap:10px}.paylater-icon{font-size:1.2em;flex-shrink:0}.paylater-text{display:flex;flex-direction:column;gap:2px}.paylater-text strong{color:#15803d;font-weight:600}.paylater-text small{color:#16a34a;font-size:.85em;opacity:.9}.paylater-desktop-only{display:none}.paylater-mobile-only{display:flex}@media (min-width:641px){.paylater-desktop-only{display:flex}.paylater-mobile-only{display:none}}@media (max-width:640px){.paylater-promo{font-size:12px;padding:7px 10px}.paylater-promo-lg{font-size:13px;padding:8px 12px}}@media print{.paylater-promo{display:none!important}}.paylater-compact{display:inline-block;font-size:11px;font-weight:600;color:#059669;background:#d1fae5;padding:3px 8px;border-radius:4px;margin:6px 0 4px;line-height:1.3}@media (max-width:640px){.paylater-compact{font-size:10px;padding:2px 6px;margin:5px 0 3px}}@media print{.paylater-compact{display:none!important}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.pd-voucher-minimal{margin:10px 0;display:flex;flex-direction:column;gap:6px}.pdv-min-item{display:flex;align-items:center;gap:8px;padding:0;height:32px;transition:all .2s ease}.pdv-min-hidden{display:none}.pdv-min-disabled{opacity:.5}.pdv-badge-btn{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border:none;border-radius:8px;padding:3px;
11284cursor:pointer;transition:all .2s ease;width:auto}.pdv-badge-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,.3)}.pdv-badge-btn:disabled{opacity:.6;cursor:not-allowed}.pdv-badge-icon{color:#fff;flex-shrink:0}.pdv-badge-text{font-size:14px;font-weight:700;color:#fff;white-space:nowrap}.pdv-badge-code{font-family:'Courier New',monospace;font-size:13px;font-weight:700;color:#fff;background:rgba(255,255,255,.2);padding:4px 8px;border-radius:4px;border:none;white-space:nowrap}.pdv-min-copy,.pdv-min-more{padding:4px 12px;font-size:12px;font-weight:600;border-radius:5px;border:1px solid #e2e8f0;background:#fff;color:#667eea;cursor:pointer;transition:all .2s ease;white-space:nowrap}.pdv-min-copy:hover:not(:disabled),.pdv-min-more:hover{background:#667eea;color:#fff;border-color:#667eea}.pdv-min-copy:disabled{opacity:.4;cursor:not-allowed}.pdv-min-copy.copied{background:#22c55e;color:#fff;border-color:#22c55e}.pdv-min-more{margin-left:auto}.modal-close{position:absolute;top:20px;right:20px;background:none;border:none;cursor:pointer;color:#64748b;padding:4px;transition:color .2s ease}.modal-close:hover{color:#1e293b}.modal-body{margin-bottom:8px}.voucher-modal-content{text-align:center}.voucher-modal-badge{display:inline-flex;flex-direction:row;align-items:center;gap:6px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border-radius:10px;padding:8px 16px;margin-bottom:12px}.vmb-value{font-size:18px;font-weight:700;color:#fff;line-height:1}.vmb-label{font-size:18px;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:.5px}.voucher-modal-code{font-family:'Courier New',monospace;font-size:14px;font-weight:700;color:#667eea;background:rgba(102,126,234,.1);padding:6px 12px;border-radius:6px;border:2px dashed rgba(102,126,234,.3);display:inline-block;margin-bottom:10px}.voucher-modal-name{font-size:14px;font-weight:600;color:#1e293b;margin-bottom:16px}.voucher-modal-details{text-align:left;background:#f8fafc;border-radius:8px;padding:12px;margin-bottom:16px}.vmd-item{display:flex;align-items:flex-start;gap:8px;padding:6px 0;font-size:13px;color:#64748b;border-bottom:1px solid #e2e8f0}.vmd-item:last-child{border-bottom:none}.vmd-item svg{color:#667eea;flex-shrink:0;margin-top:2px}.vmd-item strong{color:#1e293b}.modal-actions{padding:20px}.modal-btn-primary{background:linear-gradient(135deg,#667eea 0,#764ba2 100%);color:#fff}.modal-btn-primary:hover{opacity:.9;transform:translateY(-1px)}@media (max-width:640px){.pdv-min-item{gap:6px}.pdv-badge-btn{padding:3px;gap:6px}.pdv-badge-text{font-size:13px}.pdv-badge-code{font-size:12px;padding:3px 6px}.pdv-min-copy,.pdv-min-more{padding:3px 10px;font-size:11px}.modal-dialog{max-width:95%;margin:20px}.vmb-value{font-size:16px}.vmb-label{font-size:16px}.voucher-modal-code{font-size:13px;padding:5px 10px}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.promo-endyear-mini-banner{display:flex;align-items:center;gap:12px;padding:12px 16px;background:rgba(255,255,255,.8);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:2px solid rgba(242,140,26,.2);border-radius:12px;margin:16px 0;box-shadow:0 4px 16px rgba(0,0,0,.08);transition:all .3s ease}.promo-endyear-mini-banner:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.12);border-color:rgba(242,140,26,.4)}.pey-icon{font-size:32px;line-height:1;flex-shrink:0}.pey-content{flex:1;min-width:0}.pey-content strong{display:block;font-size:14px;font-weight:700;color:#F28C1A;margin-bottom:2px}.pey-content span{display:block;font-size:13px;color:#64748b;line-height:1.4}.pey-link{flex-shrink:0;padding:8px 16px;background:linear-gradient(135deg,#F28C1A,#FFB84D);color:#fff;text-decoration:none;font-size:13px;font-weight:700;border-radius:8px;white-space:nowrap;transition:all .2s ease;box-shadow:0 2px 8px rgba(242,140,26,.25)}.pey-link:hover{transform:translateX(2px);box-shadow:0 4px 12px rgba(242,140,26,.35)}@media(max-width:640px){.promo-endyear-mini-banner{flex-wrap:wrap;gap:8px;padding:10px 12px}.pey-icon{font-size:28px;align-self:flex-start}.pey-content{flex:1;min-width:0}.pey-content strong{font-size:12px}.pey-content span{font-size:11px;line-height:1.3}.pey-link{width:100%;flex-basis:100%;font-size:11px;padding:6px 12px;text-align:center}}@media print{.promo-endyear-mini-banner{display:none!important}}</style> 11285<style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 11286.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;
11286display: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} 11287 11288.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)}} 11289 11290.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} 11291 11292.pd-share-preview{display:flex;gap:12px;padding:16px;background:#f8fafc;border-radius:12px;margin-bottom:24px}.pd-share-preview-img{width:64px;height:64px;border-radius:8px;object-fit:cover;flex-shrink:0;border:1px solid #e2e8f0}.pd-share-preview-info{flex:1;min-width:0}.pd-share-preview-name{font-size:14px;font-weight:600;color:#0f172a;margin:0 0 4px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pd-share-preview-price{font-size:16px;font-weight:700;color:#0ea5e9;margin:0} 11293 11294.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)} 11295 11296.pd-share-icon{width:40px;height:40px;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;position:relative}.pd-share-icon svg{width:24px;height:24px;display:block}.pd-share-icon-whatsapp{background:linear-gradient(135deg,#25D366 0%,#128C7E 100%);color:#fff}.pd-share-icon-facebook{background:linear-gradient(135deg,#1877F2 0%,#0c63d4 100%);color:#fff}.pd-share-icon-twitter{background:linear-gradient(135deg,#1DA1F2 0%,#0d8bd9 100%);color:#fff}.pd-share-icon-telegram{background:linear-gradient(135deg,#0088cc 0%,#0077b3 100%);color:#fff}.pd-share-icon-email{background:linear-gradient(135deg,#EA4335 0%,#c5331f 100%);color:#fff}.pd-share-icon-copy{background:linear-gradient(135deg,#f1db63 0%,#e5d146 100%);color:#fff}.pd-share-icon-pdf{background:linear-gradient(135deg,#ef4444 0%,#dcce77 100%);color:#fff}.pd-share-name{font-size:12px;font-weight:600;color:#475569;text-align:center} 11297 11298.pd-share-copy-section{background:#f8fafc;padding:16px;border-radius:12px}.pd-share-copy-input-wrap{display:flex;gap:8px}.pd-share-copy-input{flex:1;padding:12px 14px;border:1px solid #e2e8f0;border-radius:10px;font-size:14px;color:#0f172a;background:#fff;font-family:'Segoe UI',system-ui,sans-serif}.pd-share-copy-input:focus{outline:0;border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.1)}.pd-share-copy-btn{padding:12px 20px;border:0;border-radius:10px;background:linear-gradient(135deg,#0ea5e9 0%,#0284c7 100%);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap;display:inline-flex;align-items:center;gap:6px}.pd-share-copy-btn:hover{background:linear-gradient(135deg,#0284c7 0%,#0369a1 100%);transform:translateY(-1px);box-shadow:0 4px 12px rgba(14,165,233,.3)}
11298.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} 11299 11300@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}} 11301 11302.mf-share{background:#fff;color:#fb923c;border:2px solid #fb923c;display:inline-flex;align-items:center;justify-content:center}.mf-share:active{transform:scale(.95)}.mf-share svg{display:block} 11303 11304@media(max-width:640px){.pd-share-btn{display:none!important}} 11305 11306.stock-badge.in{background:#e6f9ec;color:#15803d;border-radius:999px;padding:4px 10px;font-weight:700}.stock-badge.out{background:#d50000;color:#14b1d7;border-radius:999px;padding:4px 10px;font-weight:700} 11307 11308.stock-info-btn{--c:#0ea5e9;--b:#bae6fd;--bg:#f0f9ff;--bg-h:#e0f2fe;--b-h:#0ea5e9;margin-left:0;display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;padding:4px 10px;border-radius:999px;border:1.5px solid var(--b);background:var(--bg);color:var(--c);cursor:pointer;font-size:12px;font-weight:600;transition:all .2s ease;-webkit-tap-highlight-color:transparent}.stock-info-btn:hover{background:var(--bg-h);border-color:var(--b-h);color:#0284c7;transform:translateY(-1px);box-shadow:0 2px 8px rgba(14,165,233,.15)}.stock-info-btn:active{transform:translateY(0)}.stock-info-btn:focus-visible{outline:0;box-shadow:0 0 0 3px rgba(14,165,233,.2)}.stock-info-btn svg{width:14px;height:14px;flex-shrink:0}.stock-info-btn .btn-text{white-space:nowrap} 11309 11310.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} 11311 11312.sp-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.sp-item{display:grid;grid-template-columns:1fr auto 32px;column-gap:12px;align-items:center;padding:10px 10px;min-height:44px;border-radius:10px;background:rgba(255,255,255,.03)}.sp-left{display:flex;align-items:center;gap:10px}.sp-dot{width:10px;height:10px;border-radius:999px;background:var(--dot,#60a5fa);display:inline-block}.sp-label{font-weight:600;font-size:13px} 11313 11314 11315.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} 11316 11317.sp-wa{ 11318 grid-column:3; justify-self:end; 11319 width:32px; height:32px; min-width:32px; min-height:32px; 11320 box-sizing:border-box; 11321 padding:0; line-height:0; text-decoration:none; outline:0; 11322 display:inline-flex; align-items:center; justify-content:center; 11323 border-radius:999px; border:1px solid rgba(37,211,102,.60); /* 1px untuk hasil âcrispâ */ 11324 background:rgba(37,211,102,.08); color:#25D366; 11325 transition:background-color .15s ease, border-color .15s ease, transform .05s ease; 11326 -webkit-tap-highlight-color:transparent; 11327} 11328.sp-wa:hover{background:rgba(37,211,102,.16);border-color:rgba(37,211,102,.75)} 11329.sp-wa:active{transform:translateY(.5px) scale(.98)} 11330 11331.sp-wa svg{width:18px;height:18px;display:block;vertical-align:middle} 11332.stock-popover a{text-decoration:none} 11333 11334#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}} 11335 11336.stock-dot{position:absolute;bottom:8px;right:8px;width:8px;height:8px;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,0.4);z-index:2;cursor:help}.stock-dot.ready{background:#10b981}.stock-dot.low{background:#f59e0b}.stock-dot.out{background:#ef4444}.stock-dot:hover{transform:scale(1.2);transition:transform 0.2s ease}@media(max-width:640px){.stock-dot{width:7px;height:7px;bottom:6px;right:6px}} 11337 11338.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}} 11339 11340.pwp-trigger-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 4px;background:linear-gradient(135deg,#fbbf24 0%,#f59e0b 100%);color:#78350f;border-radius:6px;font-size:.65rem;font-weight:600;border:0}.pwp-trigger-badge svg{flex-shrink:0;width:12px;height:12px} 11341.pwp-deals-section{margin:0;padding:12px;background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border-radius:12px;border:2px solid #fbbf24}.pwp-deals-header{display:flex;gap:10px;margin-bottom:10px}.pwp-icon{flex-shrink:0;color:#f59e0b;width:18px;height:18px}.pwp-text .pwp-title{margin:0 0 2px 0;font-size:14px;font-weight:700;color:#78350f}.pwp-text .pwp-subtitle{margin:0;font-size:11px;color:#92400e;opacity:0.9}.pwp-deals-loading{text-align:center;padding:40px 0;color:#92400e}.pwp-spinner{width:40px;height:40px;border:3px solid #fbbf24;border-top-color:#f59e0b;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 12px}@keyframes spin{to{transform:rotate(360deg)}} 11342.pwp-deals-grid{display:flex;flex-direction:column;gap:6px}.pwp-deal-card{background:#fff;border-radius:6px;overflow:hidden;border:1px solid #fbbf24;display:flex;align-items:center;padding:8px;gap:10px}.pwp-deal-card:hover{background:#fef3c7}.pwp-deal-thumb{flex-shrink:0;width:50px;height:50px;background:#f8fafc;overflow:hidden;border-radius:4px}.pwp-deal-thumb img{width:100%;height:100%;object-fit:cover}.pwp-deal-badge{background:#dcce77;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:#dcce77;white-space:nowrap}.pwp-deal-original{font-size:11px;color:#64748b;text-decoration:line-through;white-space:nowrap}.pwp-cart-note{margin-top:8px;padding:6px 10px;background:#e0f2fe;border-left:2px solid#0ea5e9;font-size:11px;color:#0c4a6e;border-radius:4px} 11343@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}} 11344 11345.preorder-badge{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;background:linear-gradient(135deg,#8b5cf6 0%,#7c3aed 100%);color:#fff;border-radius:8px;font-size:13px;font-weight:700;border:0;box-shadow:0 2px 8px rgba(139,92,246,.25)}.preorder-badge svg{width:16px;height:16px;flex-shrink:0}.preorder-info{margin:12px 0;padding:14px;background:linear-gradient(135deg,#f5f3ff 0%,#ede9fe 100%);border:1.5px solid #c4b5fd;border-radius:12px}.preorder-info-header{display:flex;align-items:center;gap:8px;margin-bottom:10px}.preorder-info-icon{width:20px;height:20px;color:#7c3aed;flex-shrink:0}.preorder-info-title{font-size:14px;font-weight:700;color:#5b21b6;margin:0}.preorder-info-items{display:flex;flex-direction:column;gap:8px}.preorder-info-row{display:flex;align-items:center;justify-content:space-between;font-size:13px}.preorder-info-label{color:#6b21a8;font-weight:600}.preorder-info-value{color:#5b21b6;font-weight:700}.preorder-info-value.highlight{color:#dcce77;font-size:14px}.preorder-info-note{margin-top:10px;padding:8px 10px;background:#fef3c7;border-left:2px solid #f59e0b;font-size:12px;color:#78350f;border-radius:6px;line-height:1.4}@media(max-width:640px){.preorder-badge{font-size:12px;padding:5px 10px}.preorder-info{margin:10px 0;padding:12px}.preorder-info-title{font-size:13px}.preorder-info-row{font-size:12px}.preorder-info-note{font-size:11px;padding:6px 8px}} 11346</style> 11347 11348<div class="pd-sticky-header" id="pd-sticky-header"><div class="pd-sticky-header-container"><div class="pd-sticky-thumb"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" id="pd-sticky-thumb-img"></div><div class="pd-sticky-info"><h2 class="pd-sticky-title" id="pd-sticky-title">Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Re
11348smi</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"><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> 11349 11350<main class="product-single product-single--v3"><nav class="pd-breadcrumb" aria-label="breadcrumb"><a href="https://www.bim.miami/">BERLINTOTO</a><span>-</span><a href="https://www.bim.miami/">BERLIN TOTO</a><span>-</span><a href="https://www.bim.miami/">LOGIN BERLINTOTO</a><span>-</span><a href="https://www.bim.miami/">DAFTAR BERLINTOTO</a><span>-</span><a href="https://www.bim.miami/">LINK BERLINTOTO</a><span>-</span><span class="current"> 11351 11352</span></nav> 11353 11354<div class="pd-top-layout"><section class="pd-media-card"><figure class="pd-main-image" id="pd-main-figure"><img id="pd-main-img" src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" data-slug="" loading="eager"> 11355</figure> 11356 11357<div class="pd-mobile-slider-wrapper"> 11358<div class="pd-mobile-slider" id="pd-mobile-slider" aria-label="Galeri gambar (geser)"><div class="pd-mslide"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="eager"></div><div class="pd-mslide"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></div><div class="pd-mslide"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></div></div><div class="pd-gallery-counter" id="pd-gallery-counter">1/6</div></div> 11359<div class="pd-thumb-gallery" id="pd-thumb-gallery" aria-label="Galeri gambar produk"><button type="button" class="pd-thumb-btn active" data-full="https://www.bim.miami/berlintoto.png" data-alt="BERLINTOTO" data-slug="" aria-label="Gambar 1"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://www.bim.miami/berlintoto.png" data-alt="BERLINTOTO" data-slug="https://www.bim.miami/berlintoto.png" aria-label="Gambar 2"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://www.bim.miami/berlintoto.png" data-alt
11359="BERLINTOTO" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp" aria-label="Gambar 3"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://www.bim.miami/berlintoto.png" data-alt="BERLINTOTO" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp" aria-label="Gambar 4"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://www.bim.miami/berlintoto.png" data-alt="BERLINTOTO" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp" aria-label="Gambar 5"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://www.bim.miami/berlintoto.png" data-alt="BERLINTOTO" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-892.webp" aria-label="Gambar 6"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" 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"> 11360 <h1 class="pd-title" id="pd-title">Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi</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> 11361<div id="pd-price-anchor"> 11362</div> 11363<div class="pd-price-qr-container"> 11364 <div class="pd-price-block" id="pd-price-block"> 11365 <div class="pd-price-row"> 11366 <span class="pd-price-current" id="pd-price-current" >Rp 10.000</span> 11367 <span class="pd-price-original" id="pd-price-original">Rp 50.000</span> 11368 <span class="pd-discount-badge" id="pd-discount-badge">-90%</span></div> 11369 <div class="pd-meta-row"> 11370 <span class="sold-inline" id="pd-sold-inline" data-role="sold-chip">Terjual Terjual 5</span> 11371 <span class="pwp-trigger-badge" id="pwp-trigger-badge" hidden> 11372 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 11373 <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> 11374 <div class="pd-meta-inline"><span class="stock-badge out" id="pd-stock-badge">BERLINTOTO</span> 11375 <span class="sku">SKU: <span id="pd-sku">BANDAR TOGEL</span></span></div> 11376<div class="paylater-promo paylater-promo-sm paylater-mobile-only"> 11377 <span class="paylater-icon">ð³</span> 11378 <span class="paylater-text"> 11379 <strong>TOGEL</strong> 11380 <small>ONLINE</small> 11381 </span> 11382</div> 11383 11384<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"> 11385<li class="sp-item"><div class="sp-left"><span class="sp-dot" style="--dot:#60a5fa;"></span><span class="sp-label">Asia Mega Mas</span></div><span class="sp-qty" id="sp-qty-meg">0</span><a class="sp-wa" href="https://wa.me/6281380009898?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Asia%20Mega%20Mas%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8as
11385p2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" target="_blank" rel="noopener" aria-label="WhatsApp Asia Mega Mas"><svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/></svg></a></li> 11386 11387 <li class="sp-item"> 11388 <div class="sp-left"> 11389 <span class="sp-dot" style="--dot:#34d399;"></span> 11390 <span class="sp-label">Medan Fair</span> 11391 </div> 11392 <span class="sp-qty" id="sp-qty-mdf">0</span> 11393 <a class="sp-wa" 11394 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" 11395 target="_blank" rel="noopener" aria-label="WhatsApp Medan Fair"> 11396 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11397 <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"/> 11398 </svg> 11399 </a> 11400 </li> 11401 11402 <li class="sp-item"> 11403 <div class="sp-left"> 11404 <span class="sp-dot" style="--dot:#f59e0b;"></span> 11405 <span class="sp-label">Banda Aceh</span> 11406 </div> 11407 <span class="sp-qty" id="sp-qty-bda">0</span> 11408 <a class="sp-wa" 11409 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" 11410 target="_blank" rel="noopener" aria-label="WhatsApp Banda Aceh"> 11411 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
11412 <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"/> 11413 </svg> 11414 </a> 11415 </li> 11416 </ul> 11417 </div> 11418 11419 </div> 11420 <div class="pd-qr-print" style="display:none;"><div class="pd-qr-label">Scan untuk lihat produk:</div><img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" alt="QR Code" class="pd-qr-image"></div> 11421 </div> 11422 <div id="pd-variants-anchor"></div> 11423 11424 11425 11426 <div class="pd-compare-section"> 11427 <button type="button" class="pd-compare-btn" id="pd-compare-btn" data-product-id="325"> 11428 <svg class="compare-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11429 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 11430 </svg> 11431 <span class="compare-text">Bandingkan Produk</span> 11432 <span class="compare-remove" hidden> 11433 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11434 <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"/> 11435 </svg> 11436 Hapus dari Perbandingan 11437 </span> 11438 </button> 11439 <div class="pd-compare-info"> 11440 <small class="compare-note" id="compare-note">Bandingkan hingga 5 produk dengan bantuan AI</small> 11441 </div> 11442 </div> 11443 11444 <div class="pd-short-desc"><p><strong>Informasi BERLINTOTO:</strong></p><ul><li>Minimal Deposit: Rp.5.000,-</li><li>Jenis Permainan: Slot Gacor</li><li>Metode Pembayaran: Bank, QRIS, E-Wallet</li><li>Mata Uang: IDR (Indonesian Rupiah)</li><li>Jam Operasional: 24 Jam Nonstop</li><li>Total Rating: 399.999.999</li><li>Rating: âââââ</li><li>Daftar Buruan: <a href="https://bim.miami/aboutus/">Klik Disini</a></li></ul></div><div class="BERLINTOTO-container-v1 mini"> 11445<div class="BERLINTOTO-container"> 11446 11447 <!-- HEADER --> 11448<div class="togel-container-v3 mini"> 11449 <div class="togel-machine-v3"> 11450 <h2 class="togel-title-v3">ð¯ PREDIKSI TOGEL RESMI (SIMULASI) ð¯</h2> 11451 11452 <div class="togel-controls-v3"> 11453 <label> 11454 PASARAN 11455 <select id="marketFilter"> 11456 <option value="ALL">ALL PASARAN</option> 11457 </select> 11458 </label> 11459 11460 <label> 11461 TIPE 11462 <select id="typeFilter"> 11463 <option value="4D">4D</option> 11464 <option value="3D">3D</option> 11465 <option value="2D">2D</option> 11466 </select> 11467 </label> 11468 11469 <label> 11470 MIN CONF 11471 <input id="minConf" type="number" min="50" max="99" step="1" value="70"> 11472 </label> 11473 11474 <button id="btnTogelRandom" type="button">
11475 <span class="icon">ð²</span> GENERATE 11476 </button> 11477 </div> 11478 11479 <div class="togel-main-result-v3" id="togelResult"> 11480 <div class="togel-status-v3" id="togelStatus">â READY â</div> 11481 11482 <div class="togel-highlight-box"> 11483 <div class="togel-col-left"> 11484 <div class="k">Pasaran Rekomendasi</div> 11485 <div class="v-game" id="rMarket">-</div> 11486 11487 <div class="provider-info"> 11488 <span class="k-small">Tipe:</span> 11489 <span class="v-small" id="rType">-</span> 11490 <span class="k-small" style="margin-left:10px;">Conf:</span> 11491 <span class="v-small" id="rConf">-</span> 11492 </div> 11493 </div> 11494 11495 <div class="togel-col-right"> 11496 <div class="k-rtp">Angka Utama</div> 11497 <div class="v-rtp-huge" id="rAngka">-</div> 11498 </div> 11499 </div> 11500 11501 <div class="togel-details-v3"> 11502 <div class="togel-detail-item"> 11503 <div class="k-detail">BBFS</div> 11504 <div class="v-detail" id="rBBFS">-</div> 11505 </div> 11506 <div class="togel-detail-item"> 11507 <div class="k-detail">Angka Ikut</div> 11508 <div class="v-detail" id="rIkut">-</div> 11509 </div> 11510 <div class="togel-detail-item"> 11511 <div class="k-detail">Shio</div> 11512 <div class="v-detail" id="rShio">-</div> 11513 </div> 11514 </div> 11515 11516 <div class="togel-tip-v3" id="rTip"> 11517 Tips: Ini simulasi hiburan. Tetap bijak & disiplin. 11518 </div> 11519 </div> 11520 11521 <div class="togel-alts-v3"> 11522 <div class="togel-alts-title-v3">3 Alternatif Angka</div> 11523 <div class="togel-alt-list-v3" id="togelAltList"></div> 11524 </div> 11525 11526 <div class="togel-note-v3"> 11527 Disclaimer: Hasil di halaman ini random (simulasi), bukan acuan hasil resmi. 11528 </div> 11529 </div> 11530 </div> 11531 11532 <style> 11533 @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@600;700;800&display=swap"); 11534 11535 /* =============================== 11536 WRAPPER 11537================================ */ 11538 .togel-container-v3 { 11539 display: flex; 11540 justify-content: center; 11541 padding: 16px 10px; 11542 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 11543 } 11544 11545 /* =============================== 11546 MACHINE CORE (RED ELEGANT) 11547================================ */ 11548 .togel-machine-v3 { 11549 position: relative; 11550 width: 100%; 11551 max-width: 520px; 11552 padding: 16px; 11553 border-radius: 16px; 11554 overflow: hidden; 11555 isolation: isolate; 11556 background: radial-gradient(circle at 15% 0%, rgba(161 153 0), transparent 48%), radial-gradient(circle at 85% 100%, rgba(180, 0, 0, 0.18), transparent 55%), linear-gradient(123deg, #000000, #000000); 11557 border: 1px solid rgba(185 162 0); 11558 box-shadow: inset 0 0 18px rgba(196 199 0), 0 0 18px rgba(196 199 0); 11559 color: #dbdbdb; 11560 } 11561 11562 /* HUD SWEEP */ 11563 .togel-machine-v3::before { 11564 content: ""; 11565 position: absolute; 11566 inset: 0; 11567 background: linear-gradient(120deg, transparent 40%, rgba(255, 0, 0, .10), transparent 65%); 11568 animation: hudSweep 7s linear infinite; 11569 pointer-events: none; 11570 opacity: .8; 11571 } 11572 11573 /* =============================== 11574 TITLE 11575================================ */ 11576 .togel-title-v3 { 11577 text-align: center; 11578 margin: 0 0 12px; 11579 font-size: 15px; 11580 font-weight: 900; 11581 letter-spacing: 1px; 11582 text-shadow: 0 0 6px rgba(167 0 0 / 90%); 11583 } 11584 11585 /* =============================== 11586 CONTROLS 11587================================ */
11588 .togel-controls-v3 { 11589 display: grid; 11590 grid-template-columns: 1fr 1fr; 11591 gap: 10px; 11592 padding: 12px; 11593 border-radius: 14px; 11594 11595 background: rgba(0, 0, 0, 0.4); 11596 border: 1px solid rgba(255 240 60 / 45%); 11597 box-shadow: inset 0 0 14px rgba(205 189 0 / 22%); 11598 } 11599 11600 .togel-controls-v3 label { 11601 display: grid; 11602 gap: 6px; 11603 font-size: 10px; 11604 letter-spacing: .8px; 11605 text-transform: uppercase; 11606 color: #cdc800; 11607 } 11608 11609 .togel-controls-v3 select, 11610 .togel-controls-v3 input { 11611 background: rgba(0 0 0 / 92%); 11612 color: #c5c5c5; 11613 border: 1px solid rgba(205 189 0); 11614 border-radius: 10px; 11615 padding: 7px 9px; 11616 font-weight: 700; 11617 font-size: 12px; 11618 outline: none; 11619 box-shadow: inset 0 0 10px rgba(207 163 3); 11620 } 11621 11622 /* BUTTON */ 11623 #btnTogelRandom { 11624 grid-column: span 2; 11625 display: flex; 11626 align-items: center; 11627 justify-content: center; 11628 gap: 8px; 11629 11630 background: linear-gradient(145deg, #dcce77, #dcce77); 11631 border: none; 11632 color: #000000; 11633 font-weight: 900; 11634 letter-spacing: .8px; 11635 border-radius: 14px; 11636 cursor: pointer; 11637 padding: 10px 12px; 11638 font-size: 12px; 11639 11640 box-shadow: 0 0 16px rgba(255 240 60 / 45%); 11641 transition: .2s ease; 11642 } 11643 11644 #btnTogelRandom:active { 11645 transform: scale(.98); 11646 } 11647 11648 #btnTogelRandom:disabled { 11649 opacity: .75; 11650 cursor: not-allowed; 11651 } 11652 11653 /* =============================== 11654 MAIN RESULT 11655================================ */ 11656 .togel-main-result-v3 { 11657 margin-top: 12px; 11658 padding: 12px; 11659 border-radius: 14px; 11660 11661 background: rgba(30, 0, 0, 0.78); 11662 border: 1px solid rgba(196 199 0); 11663 box-shadow: inset 0 0 16px rgba(196 199 0); 11664 } 11665 11666 .togel-status-v3 { 11667 text-align: center; 11668 font-weight: 800; 11669 letter-spacing: .8px; 11670 color: #cdc800; 11671 margin-bottom: 10px; 11672 font-size: 11px; 11673 } 11674 11675 .togel-highlight-box { 11676 display: flex; 11677 justify-content: space-between; 11678 gap: 12px; 11679 padding-bottom: 10px; 11680 margin-bottom: 10px; 11681 border-bottom: 1px dashed rgba(255 242 80 / 30%); 11682 } 11683 11684 .k, 11685 .k-rtp { 11686 font-size: 10px; 11687 color: #c7c7c7; 11688 text-transform: uppercase; 11689 } 11690 11691 .v-game { 11692 font-size: 14px; 11693 font-weight: 900; 11694 color: #ffffff; 11695 margin-top: 6px; 11696 line-height: 1.2; 11697 } 11698 11699 .provider-info { 11700 margin-top: 8px; 11701 font-size: 11px; 11702 color: #d9d9d9; 11703 opacity: .95; 11704 } 11705 11706 .k-small { 11707 color: #cdc800; 11708 font-size: 10px; 11709 } 11710 11711 .v-small {
11712 color: #fff0f0; 11713 font-weight: 900; 11714 } 11715 11716 .v-rtp-huge { 11717 font-size: 2.4em; 11718 font-weight: 900; 11719 color: #d7ab00; 11720 text-shadow: 0 0 10px rgba(0 0 0); 11721 line-height: 1; 11722 letter-spacing: 1px; 11723 } 11724 11725 /* DETAILS */ 11726 .togel-details-v3 { 11727 display: grid; 11728 grid-template-columns: repeat(3, 1fr); 11729 gap: 8px; 11730 background: rgba(0, 0, 0, .30); 11731 padding: 10px; 11732 border-radius: 12px; 11733 } 11734 11735 .k-detail { 11736 font-size: 9px; 11737 color: #cdc800; 11738 text-transform: uppercase; 11739 } 11740 11741 .v-detail { 11742 font-size: 12px; 11743 font-weight: 800; 11744 color: #ffffff; 11745 margin-top: 4px; 11746 letter-spacing: .8px; 11747 } 11748 11749 .togel-tip-v3 { 11750 margin-top: 10px; 11751 font-size: 10px; 11752 color: #d9d9d9; 11753 opacity: .9; 11754 text-align: center; 11755 } 11756 11757 /* ALTS */ 11758 .togel-alts-v3 { 11759 margin-top: 12px; 11760 padding: 12px; 11761 border-radius: 14px; 11762 background: rgba(0, 0, 0, .40); 11763 border: 1px solid rgba(196 199 0); 11764 } 11765 11766 .togel-alts-title-v3 { 11767 font-weight: 900; 11768 color: #cdc800; 11769 margin-bottom: 10px; 11770 font-size: 12px; 11771 } 11772 11773 .togel-alt-list-v3 { 11774 display: grid; 11775 grid-template-columns: 1fr; 11776 gap: 8px; 11777 } 11778 11779 .togel-alt-item { 11780 background: rgba(30, 0, 0, .86); 11781 border-radius: 12px; 11782 padding: 10px; 11783 border-left: 3px solid rgba(167 0 0 / 90%); 11784 } 11785 11786 .togel-alt-item .g { 11787 font-weight: 900; 11788 color: #c5c5c5; 11789 font-size: 12px; 11790 } 11791 11792 .togel-alt-item .p { 11793 font-size: 10px; 11794 color: #cdc800; 11795 margin-top: 4px; 11796 } 11797 11798 .togel-alt-item .r { 11799 margin-top: 6px; 11800 font-weight: 900; 11801 color: #d7ab00; 11802 letter-spacing: .8px; 11803 font-size: 12px; 11804 } 11805 11806 .togel-note-v3 { 11807 margin-top: 10px; 11808 font-size: 10px; 11809 color: #d9d9d9; 11810 opacity: .85; 11811 text-align: center; 11812 } 11813 11814 /* MOBILE */ 11815 @media(max-width:420px) { 11816 .togel-highlight-box { 11817 flex-direction: column 11818 } 11819 11820 .togel-details-v3 { 11821 grid-template-columns: 1fr 11822 } 11823 } 11824 11825 /* ANIM */ 11826 @keyframes hudSweep { 11827 0% { 11828 transform: translateX(-110%) 11829 } 11830 11831 100% { 11832 transform: translateX(110%) 11833 } 11834 } 11835 11836 @keyframes pulse { 11837 0% { 11838 opacity: .55 11839 } 11840 11841 50% { 11842 opacity: 1 11843 } 11844 11845 100% { 11846 opacity: .55 11847 } 11848 } 11849 </style> 11850 11851
11851<script> 11852 /* =============================== 11853 DATASET TOGEL (SIMULASI) 11854 ================================ */ 11855 const TOGEL_MARKETS = [ 11856 { market: "Singapore", code: "SGP", open: "17:45", zone: "WIB" }, 11857 { market: "Hongkong", code: "HK", open: "22:45", zone: "WIB" }, 11858 { market: "Sydney", code: "SDY", open: "13:45", zone: "WIB" }, 11859 { market: "Macau", code: "MC", open: "16:00", zone: "WIB" }, 11860 { market: "Cambodia", code: "KHM", open: "10:45", zone: "WIB" }, 11861 { market: "Japan", code: "JP", open: "11:30", zone: "WIB" } 11862 ]; 11863 11864 const SHIO = ["Tikus", "Kerbau", "Macan", "Kelinci", "Naga", "Ular", "Kuda", "Kambing", "Monyet", "Ayam", "Anjing", "Babi"]; 11865 11866 const $ = (q) => document.querySelector(q); 11867 const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 11868 11869 function uniqueMarkets() { 11870 return TOGEL_MARKETS.map(m => m.market); 11871 } 11872 11873 function genDigits(len) { 11874 let s = ""; 11875 for (let i = 0; i < len; i++) s += String(randInt(0, 9)); 11876 return s; 11877 } 11878 11879 function genBBFS() { 11880 const pool = [...Array(10)].map((_, i) => i); 11881 const out = []; 11882 for (let i = 0; i < 7; i++) { 11883 const idx = randInt(0, pool.length - 1); 11884 out.push(pool.splice(idx, 1)[0]); 11885 } 11886 return out.join(""); 11887 } 11888 11889 function genIkut() { 11890 const pool = [...Array(10)].map((_, i) => i); 11891 const out = []; 11892 for (let i = 0; i < 4; i++) { 11893 const idx = randInt(0, pool.length - 1); 11894 out.push(pool.splice(idx, 1)[0]); 11895 } 11896 return out.join(" ⢠"); 11897 } 11898 11899 function simulateConfidence() { 11900 return randInt(60, 96); 11901 } 11902 11903 function applyFilters() { 11904 const m = $("#marketFilter").value; 11905 const minC = parseFloat($("#minConf").value || "0"); 11906 11907 return TOGEL_MARKETS 11908 .filter(x => (m === "ALL" || x.market === m)) 11909 .map(x => ({ ...x, conf: simulateConfidence() })) 11910 .filter(x => x.conf >= minC); 11911 } 11912 11913 function pickRandom(arr, n = 1) { 11914 const copy = [...arr]; 11915 const out = []; 11916 for (let i = 0; i < n && copy.length; i++) { 11917 const idx = randInt(0, copy.length - 1); 11918 out.push(copy.splice(idx, 1)[0]); 11919 } 11920 return out; 11921 } 11922 11923 function formatAngka(type) { 11924 if (type === "4D") return genDigits(4); 11925 if (type === "3D") return genDigits(3); 11926 return genDigits(2); 11927 } 11928 11929 function renderMain(item, type) { 11930 $("#rMarket").textContent = `${item.market} (${item.code}) ⢠Open ${item.open} ${item.zone}`; 11931 $("#rType").textContent = type; 11932 $("#rConf").textContent = `${item.conf}%`; 11933 11934 $("#rAngka").textContent = formatAngka(type); 11935 $("#rBBFS").textContent = genBBFS(); 11936 $("#rIkut").textContent = genIkut(); 11937 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 11938 $("#rTip").textContent = "Tips: Ini simulasi hiburan. Tetap bijak & disiplin."; 11939 } 11940 11941 function renderAlts(list, type) { 11942 const wrap = $("#togelAltList"); 11943 wrap.innerHTML = ""; 11944 list.forEach(x => { 11945 const el = document.createElement("div"); 11946 el.className = "togel-alt-item"; 11947 el.innerHTML = ` 11948 <div class="g">${x.market} (${x.code})</div> 11949 <div class="p">Tipe ${type} ⢠Conf <strong>${x.conf}%</strong></div> 11950 <div class="r">Angka: <strong>${formatAngka(type)}</strong></div> 11951 `; 11952 wrap.appendChild(el); 11953 }); 11954 } 11955 11956 function spinButton(btn, on) { 11957 if (on) { 11958 btn.disabled = true; 11959 btn.dataset.text = btn.innerHTML; 11960 btn.innerHTML = `<span class="icon">ð</span> PROCESS...`; 11961 } else { 11962 btn.disabled = false;
11963 btn.innerHTML = btn.dataset.text || `<span class="icon">ð²</span> GENERATE`; 11964 } 11965 } 11966 11967 /* =============================== 11968 INIT 11969 ================================ */ 11970 (function init() { 11971 const sel = $("#marketFilter"); 11972 uniqueMarkets().forEach(m => { 11973 const o = document.createElement("option"); 11974 o.value = m; o.textContent = m; 11975 sel.appendChild(o); 11976 }); 11977 11978 $("#btnTogelRandom").addEventListener("click", (e) => { 11979 const btn = e.currentTarget; 11980 const type = $("#typeFilter").value; 11981 const filtered = applyFilters(); 11982 const st = $("#togelStatus"); 11983 11984 if (!filtered.length) { 11985 st.textContent = "â NO MATCH â"; 11986 st.style.color = "white"; 11987 renderAlts([], type); 11988 $("#rMarket").textContent = "-"; 11989 $("#rType").textContent = type; 11990 $("#rConf").textContent = "-"; 11991 $("#rAngka").textContent = "-"; 11992 $("#rBBFS").textContent = "-"; 11993 $("#rIkut").textContent = "-"; 11994 $("#rShio").textContent = "-"; 11995 return; 11996 } 11997 11998 spinButton(btn, true); 11999 st.textContent = "â GENERATING â"; 12000 st.style.animation = "pulse .6s linear infinite"; 12001 st.style.color = "white"; 12002 12003 const t0 = setInterval(() => { 12004 const temp = pickRandom(filtered, 1)[0]; 12005 $("#rMarket").textContent = `${temp.market} (${temp.code}) ⢠Open ${temp.open} ${temp.zone}`; 12006 $("#rType").textContent = type; 12007 $("#rConf").textContent = `${temp.conf}%`; 12008 $("#rAngka").textContent = formatAngka(type); 12009 $("#rBBFS").textContent = genBBFS(); 12010 $("#rIkut").textContent = genIkut(); 12011 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 12012 }, 85); 12013 12014 setTimeout(() => { 12015 clearInterval(t0); 12016 12017 const picks = pickRandom(filtered, 4); 12018 renderMain(picks[0], type); 12019 renderAlts(picks.slice(1), type); 12020 12021 st.textContent = "â READY â"; 12022 st.style.animation = ""; 12023 st.style.color = "gold"; 12024 spinButton(btn, false); 12025 }, 1200); 12026 }); 12027 12028 renderAlts([], "4D"); 12029 })(); 12030 </script>
12030 12031
12032<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: 1 bytes, line 12032
12032
12033<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: 1 bytes, line 12033
12033
12034<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: 1 bytes, line 12034
12034
12035<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: 1 bytes, line 12035
12035
12036<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: 1 bytes, line 12036
12036
12037<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: 1 bytes, line 12037
12037
12038<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>
12038 12039 12040<style> 12041body{background:#ebf2fc; font-family:Poppins,sans-serif; color:white;} 12042.BERLINTOTO-container{ 12043max-width: 520px; 12044 margin: auto; 12045 padding: 15px; 12046 border-radius: 12px; 12047 background: #a19508; 12048 border: 2px solid #c7ca00; 12049 box-shadow: 0 0 20px #d7d700; 12050} 12051 12052.BERLINTOTO-header{ 12053 font-weight:900; 12054 text-align:center; 12055 color:#ff4c4c; 12056 text-shadow:0 0 12px #5f90d9; 12057 margin-bottom:12px; 12058} 12059 12060.BERLINTOTO-controls{ 12061 display:grid; 12062 grid-template-columns:1fr 1fr; 12063 gap:10px; 12064 margin-bottom:12px; 12065} 12066.BERLINTOTO-controls label{ 12067 display:flex; flex-direction:column; 12068 font-size:11px; color:#ff9999; 12069} 12070.BERLINTOTO-controls select, 12071.BERLINTOTO-controls input{ 12072 padding:8px; 12073 border-radius:8px; 12074 border:1px solid #ff4c4c; 12075 background:#2b0000; 12076 color:white; 12077} 12078#btnGenerate{ 12079 grid-column:span 2; 12080 padding:12px; 12081 background:#5f90d9; 12082 border:none; 12083 color:black; 12084 font-weight:900; 12085 cursor:pointer; 12086 margin-top:5px; 12087 box-shadow:0 0 15px #ff4c4c; 12088} 12089 12090.BERLINTOTO-result{ 12091 margin-top:12px; 12092 padding:12px; 12093 border-radius:10px; 12094 background:rgba(255,0,0,0.05); 12095 border:1px solid #ff4c4c; 12096} 12097 12098.BERLINTOTO-main div, .BERLINTOTO-details div{ 12099 margin:5px 0; 12100} 12101 12102#rPrediction{ 12103 font-size:24px; 12104 font-weight:900; 12105 color:#5f90d9; 12106} 12107 12108.BERLINTOTO-alts{ 12109 margin-top:10px; 12110} 12111.BERLINTOTO-alts h4{margin-bottom:6px;} 12112#BERLINTOTOAltList div{ 12113 background:#2b0000; 12114 padding:6px 10px; 12115 margin:3px 0; 12116 border-radius:8px; 12117 border-left:3px solid #5f90d9; 12118} 12119 .bank-tab.active, .bank-tab:hover { 12120 background: #df0000; 12121 color: #fff; 12122 border-color: #df0000; 12123 box-shadow: 0 3px 10px -4px rgb(240 29 29 / 60%); 12124} 12125 12126.BERLINTOTO-disclaimer{ 12127 font-size:10px; 12128 margin-top:8px; 12129 text-align:center; 12130 color:#ff9999; 12131} 12132</style> 12133
12134<script> 12135const leagues = ["Premier League","La Liga","Serie A"]; 12136const tips = ["Analisis statistik tim","Performa terakhir","Cek head-to-head"]; 12137function rand(arr){return arr[Math.floor(Math.random()*arr.length)];} 12138 12139var _bgBtn=document.getElementById("btnGenerate");if(_bgBtn)_bgBtn.onclick=function(){ 12140 const type = document.getElementById("betTypeFilter").value; 12141 const league = rand(leagues); 12142 12143 let pred; 12144 if(type==="1X2") pred=rand(["1","X","2"]); 12145 else if(type==="OVER_UNDER") pred=rand(["Over","Under"]); 12146 else pred=rand(["Home -1","Away +1","Draw"]); 12147 12148 document.getElementById("rLeague").innerText=league; 12149 document.getElementById("rPrediction").innerText=pred; 12150 document.getElementById("rBetType").innerText=type; 12151 document.getElementById("rConf").innerText=Math.floor(Math.random()*40+60)+"%"; 12152 document.getElementById("rTip").innerText=rand(tips); 12153 document.getElementById("rOdds").innerText=(Math.random()*3+1).toFixed(2); 12154 12155 // alternatif 12156 const alt=document.getElementById("BERLINTOTOAltList"); alt.innerHTML=""; 12157 for(let i=0;i<3;i++){let d=document.createElement("div"); d.innerText=rand(["1","X","2"]); alt.appendChild(d);} 12158 12159 document.getElementById("BERLINTOTOStatus").innerText="â READY â"; 12160} 12161</script>
12161 12162 12163 12164 12165 <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> 12166 </div> 12167 </div> 12168 </section> 12169 12170 <aside class="pd-sidebar"> 12171 <div class="pd-side-card pd-buy-card"> 12172 <form class="pd-buy-form" id="pd-buy-form" data-pid="325"> 12173 <input type="hidden" name="variant_id" id="pd-variant-id" value="0"> 12174 <div> 12175 <label for="qty" class="qty-label">Qty</label> 12176 <input type="number" id="qty" name="qty" value="0" min="1" 12177 max="0" 12178 disabled> 12179 </div> 12180 <div class="pd-actions"> 12181 <button type="submit" class="btn-add-cart" id="btn-add-cart" disabled>+ Keranjang</button> 12182 <button type="button" class="btn-buy" id="btn-buy" data-id="278426" disabled >Beli</button> 12183 <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> 12184 </div> 12185 </form> 12186 <div class="paylater-promo paylater-promo-md paylater-desktop-only"> 12187 <span class="paylater-icon">ð³</span> 12188 <span class="paylater-text"> 12189 <strong>Autorized BERLINTOTO</strong> 12190 <small>Situs Resmi BERLINTOTO </small> 12191 </span> 12192</div> </div> 12193 12194 12195 <div class="pd-side-card qoala-banner-card"> 12196 <div class="qoala-banner"> 12197 <div class="qoala-banner-content"> 12198 <div class="qoala-icon"> 12199 <center><img src="https://www.bim.miami/iconberlintoto.png" alt="BERLINTOTO" class="qoala-shield-img"></center> 12200 </div> 12201 <div class="qoala-text"> 12202 <div class="qoala-title">Login BERLINTOTO</div> 12203 <div class="qoala-subtitle">Verified</div> 12204 </div> 12205 </div> 12206 <a href="https://bim.miami/aboutus/" class="qoala-cta" 12207 data-ga-event="qoala_banner_click" 12208 data-product-id="325" 12209 data-product-name="BERLINTOTO" 12210 data-category-path="Komputer & Laptop/Laptop/Gaming" target="_blank">Info</a> 12211 </div> 12212<style> 12213.qoala-banner-card{padding:0;overflow:hidden} 12214 12215.qoala-banner{ 12216 background:linear-gradient(135deg, #cdc800 0%, #c90000 100%); 12217 padding:12px 16px; 12218 display:flex; 12219 align-items:center; 12220 justify-content:space-between; 12221 color:white; 12222 text-decoration:none; 12223 min-height:60px; 12224} 12225 12226.qoala-banner-content{ 12227 display:flex; 12228 align-items:center; 12229 gap:12px; 12230} 12231 12232.qoala-icon{ 12233 display:flex; 12234 align-items:center; 12235 justify-content:center; 12236} 12237 12238.qoala-shield-img{ 12239 width:36px; 12240 height:36px; 12241 object-fit:contain; 12242 opacity:0.9; 12243} 12244 12245.qoala-title{ 12246 font-weight:600; 12247 font-size:14px; 12248 line-height:1.2; 12249} 12250 12251.qoala-subtitle{ 12252 font-size:12px; 12253 opacity:0.9; 12254 line-height:1.2; 12255} 12256 12257.qoala-cta{ 12258 background:rgba(255,255,255,0.15); 12259 color:white; 12260 padding:6px 12px; 12261 border-radius:6px; 12262 font-size:12px; 12263 font-weight:600; 12264 text-decoration:none; 12265 transition:background 0.2s ease; 12266 border:1px solid rgba(255,80,80,0.4); 12267} 12268 12269.qoala-cta:hover{ 12270 background:rgba(255,60,60,0.25); 12271 color:white; 12272} 12273 12274@media(max-width:768px){ 12275 .qoala-banner{padding:10px 12px} 12276 .qoala-title{font-size:17px} 12277 .qoala-subtitle{font-size:11px} 12278} 12279</style> 12280 </div> 12281 12282 <div class="pd-side-card pd-installment-card2" 12283 id="inst-card" 12284 data-base-price="20789000" 12285 data-zero-smart="0"> 12286 <div class="inst-head"> 12287 <h3>Promo & Fitur BERLINTOTO</h3> 12288 </div> 12289 12290 <div class="inst-bank-tabs" role="tablist" aria-label="Bank"> 12291 <button type="button" class="bank-tab active" data-bank="Lucky Draw" aria-selected="true">Lucky Draw</button> 12292 <button type="button" class="bank-tab" data-bank="Lomba Panjat Tur
12292nover" aria-selected="false">Lomba Panjat Turnover</button> 12293 <button type="button" class="bank-tab" data-bank="Prediksi Jitu" aria-selected="false">Prediksi Jitu</button> 12294 </div> 12295 12296 <ul class="inst-month-list" id="inst-month-list" aria-label="Daftar cicilan per bulan"></ul> 12297 <div class="inst-note" id="inst-zero-note" style="display:none;">0% Cicilan khusus bank terpilih*</div> 12298 </div> 12299 12300 <!-- MECHA BLUE FINAL BOSS REVIEW SLIDER --> 12301<div class="tnt-review-mecha"> 12302 12303<div class="tnt-header"> 12304<h2>REVIEW PEMAIN TOTO TOGEL</h2> 12305 12306</div> 12307 12308<div class="tnt-slider" id="tntSlider"> 12309 12310<div class="tnt-slide active"> 12311 <div class="tnt-name">Aliong â Surabaya</div> 12312 <div class="tnt-comment">"Sangat puas pasang angka 4D di sini karena hasilnya real-time dan sesuai dengan jadwal pengeluaran resmi. Tampilan dashboard akunnya bersih, riwayat tebakan tercatat transparan, dan sistem klaim diskon togelnya langsung otomatis tanpa ribet."</div> 12313</div> 12314 12315<div class="tnt-slide"> 12316 <div class="tnt-name">Santoso88 â Semarang</div> 12317 <div class="tnt-comment">"Urusan transaksi finansial di sini jempolan banget. Deposit pakai scan QRIS instan masuk hitungan detik tanpa perlu konfirmasi manual yang berbelit-belit. Withdraw berapapun selalu diproses cepat dan transparan tanpa ada biaya potongan tersembunyi."</div> 12318</div> 12319 12320<div class="tnt-slide"> 12321 <div class="tnt-name">Wijaya â Makassar</div> 12322 <div class="tnt-comment">"Layanan Customer Service di fitur Live Chat bener-bener siaga 24 jam nonstop. Waktu itu pernah salah ketik nominal transaksi pas jam 2 malam, CS-nya langsung bantu selesaikan dengan cepat, ramah, dan solutif. Sistem keamanannya bikin tenang untuk main jangka panjang."</div> 12323</div> 12324 12325 12326 12327</div> 12328</div> 12329 12330</aside> 12331</div> 12332 12333<style> 12334.tnt-review-mecha{ 12335 background: radial-gradient(circle at center, #da0000, #350000); 12336 border: 2px solid #aa0000; 12337 border-radius: 14px; 12338 padding: 20px; 12339 color: #dbdbdb; 12340 font-family: 'Orbitron', sans-serif; 12341 position: relative; 12342 overflow: hidden; 12343 box-shadow: 0 0 25px #990000, inset 0 0 20px rgba(255, 0, 0, 0.2); 12344} 12345 12346/* scanline */ 12347.tnt-review-mecha::before{ 12348 content:""; 12349 position:absolute; 12350 inset:0; 12351 background: repeating-linear-gradient( 12352 to bottom, 12353 rgba(255,0,0,0.08), 12354 rgba(255,0,0,0.08) 1px, 12355 transparent 2px, 12356 transparent 4px 12357 ); 12358 pointer-events:none; 12359}.bank-tab.active, .bank-tab:hover 12360 12361 12362.tnt-header h2{ 12363 font-size: 18px; 12364 color:#000000; 12365 text-shadow:0 0 10px #d71414, 0 0 20px #d10000; 12366 margin-bottom:5px; 12367} 12368 12369.tnt-header p{ 12370 font-size:12px; 12371 opacity:0.85; 12372 margin-bottom:15px; 12373} 12374 12375.tnt-slider{ 12376 position:relative; 12377 height:90px; 12378} 12379 12380.tnt-slide{ 12381 position:absolute; 12382 width:100%; 12383 opacity:0; 12384 transform:translateX(40px); 12385 transition:all .6s ease; 12386} 12387 .tenor-badge { 12388 background: #df0000; 12389 color: #fff; 12390 font-weight: 700; 12391 min-width: 42px; 12392 text-align: center; 12393 padding: .38rem .4rem; 12394 font-size: .8rem; 12395 border-radius: 10px; 12396 box-shadow: 0 3px 10px -4px rgb(211 3 3); 12397} 12398 12399.tnt-slide.active{ 12400 opacity:1; 12401 transform:translateX(0); 12402} 12403 12404.tnt-name{ 12405font-size: 14px; 12406 color: #000000; 12407 margin-bottom: 5px; 12408 text-shadow: 0 0 6px #290000; 12409} 12410 12411.tnt-comment{ 12412 font-size:13px; 12413 line-height:1.5; 12414} 12415 12416/* glowing border animation */ 12417@keyframes pulseGlow{ 12418 0%{box-shadow:0 0 10px #d71414;} 12419 50%{box-shadow:0 0 25px #d71414;} 12420 100%{box-shadow:0 0 10px #d71414;} 12421} 12422 12423.tnt-review-mecha{ 12424 animation:pulseGlow 2.5s infinite; 12425} 12426</style> 12427
12428<script> 12429let slides = document.querySelectorAll('.tnt-slide'); 12430let index = 0; 12431 12432setInterval(()=>{ 12433 slides[index].classList.remove('active'); 12434 index = (index + 1) % slides.length; 12435 slides[index].classList.add('active'); 12436}, 3000); 12437</script>
12437 12438 12439 <div class="artikel-glow-KT"> 12440 12441 <div class="artikel-shine"></div> 12442 12443 <h1>Apa Itu BERLINTOTO | Situs Toto Slot Gacor & Bandar Togel Online 4D Resmi</h1> 12444 <p style="text-align: justify;"><a href="https://www.bim.miami/">BERLINTOTO</a> Hadir situs Toto slot gacor & bandar togel online 4D resmi, BERLINTOTO tampil sebagai solusi portal permainan digital serba ada yang menjawab kebutuhan para pecinta taruhan angka dan putaran mesin modern di Indonesia. Kehadiran platform ini mengusung standar layanan bertaraf internasional dengan memprioritaskan stabilitas sistem, transparansi permainan, serta kemudahan navigasi bagi seluruh member.</p> 12445 12446<p style="text-align: justify;">Infrastruktur portal ini dibangun di atas jaringan server berspesifikasi tinggi yang dilengkapi sistem keamanan enkripsi data mutakhir. Hal ini memastikan seluruh data pribadi dan riwayat transaksi finansial member terproteksi dengan sangat ketat. Selain itu, optimasi antarmuka yang ringan memungkinkan pengguna untuk mengakses beragam deretan permainan slot gacor mudah JP serta pasaran togel 4D resmi secara stabil melalui ponsel pintar (Android/iOS) maupun komputer tanpa memerlukan aplikasi tambahan seperti VPN.</p> 12447 12448 <p style="text-align: justify;">Sebagai bandar taruhan online terpercaya, penyedia layanan ini menyajikan hasil live draw pengeluaran angka togel secara real-time dan akurat langsung dari server pusat resmi. Seluruh proses transaksiâbaik deposit maupun penarikan dana (withdraw)âdijalankan secara serba otomatis melalui berbagai jalur fleksibel, seperti transfer bank lokal, e-wallet, hingga scan QRIS instan. Didukung oleh tim Customer Service profesional yang siaga selama 24 jam nonstop, situs ini menegaskan komitmennya untuk menghadirkan pengalaman bertaruh yang aman, nyaman, dan terbukti terpercaya.</p> 12449 12450</p> 12451</div> 12452 12453<style> 12454.artikel-glow-KT{ 12455 max-width: 820px; 12456 margin: 40px auto; 12457 padding: 36px; 12458 box-sizing: border-box; 12459 position: relative; 12460 overflow: hidden; 12461 border-radius: 30px; 12462 font-family: Arial, sans-serif; 12463 color: #ffffff; 12464 background: radial-gradient(circle at top left, rgba(177 2 2), transparent 20%), radial-gradient(circle at bottom right, rgb(75 0 0), transparent 28%), linear-gradient(135deg, #cd0707, #a90000, #c90303); 12465 border: 2px solid rgba(255, 255, 255, .55); 12466 box-shadow: 0 0 25px rgb(207 163 3), 0 0 55px rgb(225 0 0), 0 20px 45px rgba(0, 0, 0, .25), inset 0 0 35px rgba(145 0 0); 12467} 12468 12469.artikel-glow-KT:before{ 12470 content:""; 12471 position:absolute; 12472 inset:10px; 12473 border-radius:24px; 12474 border:1px solid rgba(255,255,255,.32); 12475 pointer-events:none; 12476} 12477 12478.artikel-shine{ 12479 position:absolute; 12480 top:-120px; 12481 left:-180px; 12482 width:280px; 12483 height:650px; 12484 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12485 transform:rotate(25deg); 12486 animation:artikelShine 4.5s infinite; 12487 pointer-events:none; 12488} 12489 12490@keyframes artikelShine{ 12491 0%{left:-220px;} 12492 55%{left:110%;} 12493 100%{left:110%;} 12494} 12495 12496.artikel-glow-KT h1, 12497.artikel-glow-KT h2{ 12498 position:relative; 12499 color:#000000; 12500 text-align:center; 12501 line-height:1.35; 12502 text-shadow: 12503 0 0 8px rgba(179 0 0 / 95%), 12504 0 0 18px rgba(197 0 0), 12505 0 2px 8px rgba(149 0 0); 12506} 12507 12508.artikel-glow-KT h1{ 12509 font-size:31px; 12510 margin:0 0 20px; 12511} 12512 12513.artikel-glow-KT h2{ 12514 font-size:24px; 12515 margin:28px 0 14px; 12516} 12517 12518.artikel-glow-KT p{ 12519 position:relative; 12520 margin:0 0 16px; 12521 color:#f8ffff; 12522 font-size:15px; 12523 line-height:1.85; 12524 text-align:justify; 12525 text-shadow:0 1px 4px rgba(0,0,0,.28); 12526} 12527 12528.artikel-glow-KT a{ 12529 color:#000000; 12530 font-weight:800; 12531 text-shadow:0 0 8px rgba(153 0 0); 12532} 12533 12534.artikel-glow-KT img{ 12535 position:relative; 12536 display:block; 12537 max-width:100%; 12538 margin:16px auto 24px; 12539 border-radius:16px; 12540 border:1px solid rgba(255,255,255,.55); 12541 box-shadow: 12542 0 0 18px rgba(255,255,255,.55), 12543 0 12px 28px rgba(0,0,0,.28); 12544} 12545 12546.artikel-highlight{ 12547 position:relative; 12548 margin:24px 0; 12549 padding:22px; 12550 border-radius:22px; 12551 background:rgba(255,255,255,.20); 12552 border:1px solid rgba(255,255,255,.45); 12553 box-shadow: 12554 inset 0 1px 0 rgba(255,255,255,.65), 12555 inset 0 0 22px rgba(255,255,255,.14), 12556 0 10px 25px rgba(0,0,0,.18); 12557 backdrop-filter:blur(8px); 12558} 12559 12560.artikel-highlight:before{ 12561 content:""; 12562 position:absolute; 12563 left:0; 12564 right:0; 12565 top:0; 12566 height:45%; 12567 border-radius:22px 22px 0 0; 12568 background:linear-gradient(rgba(255,255,255,.35),rgba(255,255,255,.04)); 12569 pointer-events:none; 12570} 12571 12572.artikel-glow-KT ul{ 12573 position:relative; 12574 margin:15px 0 0 20px; 12575 padding:0; 12576} 12577 12578.artikel-glow-KT li{ 12579 margin-bottom:11px; 12580 padding:10px 12px; 12581 border-radius:12px; 12582 background:rgba(255,255,255,.16); 12583 color:#ffffff; 12584 line-height:1.6; 12585 box-shadow:inset 0 0 10px rgba(255,255,255,.08); 12586} 12587 12588@media(max-width:600px){ 12589 .artikel-glow-KT{ 12590 margin:25px 12px; 12591 padding:25px; 12592 border-radius:24px; 12593 } 12594 12595 .artikel-glow-KT h1{ 12596 font-size:24px; 12597 } 12598 12599 .artikel-glow-KT h2{ 12600 font-size:21px; 12601 } 12602} 12603</style> 12604 12605<div class="KT-faq-rainbow"> 12606 <div class="KT-shine"></div> 12607 12608 <div class="KT-faq-top"> 12609 <span>â PREMIUM FAQ â </span> 12610 <h2>Pertanyaan Umum BERLINTOTO</h2> 12611 12612 </div> 12613 12614 <div class="KT-rainbow-item"> 12615 <button type="button">Apa yang membuat BERLINTOTO dikenal sebagai platform taruhan online terpercaya?</button> 12616 <div class="KT-rainbow-answer"> 12617 <p>Hadir situs Toto slot gacor & bandar togel online 4D resmi, BERLINTOTO menyajikan ekosistem permainan digital terlengkap yang didukung oleh teknologi server modern, stabilitas akses tanpa kendala pemblokiran, serta jaminan transparansi permainan secara menyeluruh.</p> 12618 </div> 12619 </div> 12620 12621 <div class="KT-rainbow-item"> 12622 <button type="button">Pasaran togel apa saja yang dapat dimainkan di BERLINTOTO?</button> 12623 <div class="KT-rainbow-answer"> 12624 <p>
12624BERLINTOTO menyediakan pilihan pasaran togel online 4D resmi terpopuler dan terlengkap. Seluruh hasil pengeluaran angka diputar secara live draw dan diperbarui secara real-time langsung dari server pusat yang akurat.</p> 12625 </div> 12626 </div> 12627 12628 <div class="KT-rainbow-item"> 12629 <button type="button">Bagaimana cara menggunakan BERLINTOTO untuk membuka situs toto slot dan situs toto togel?</button> 12630 <div class="KT-rainbow-answer"> 12631 <p>Keamanan member adalah prioritas utama. Seluruh infrastruktur jaringan BERLINTOTO dilindungi oleh sistem enkripsi data canggih yang menjaga kerahasiaan informasi pribadi serta menjamin keamanan penuh pada setiap riwayat transaksi finansial.</p> 12632 </div> 12633 </div> 12634 12635 <div class="KT-rainbow-item"> 12636 <button type="button">Metode pembayaran apa saja yang disediakan untuk transaksi deposit dan withdraw?</button> 12637 <div class="KT-rainbow-answer"> 12638 <p>Proses transaksi diproses secara serba otomatis dan fleksibel. Member dapat menggunakan transfer bank lokal utama, fitur scan QRIS instan, hingga berbagai opsi e-wallet populer untuk melakukan pengisian saldo maupun penarikan dana.</p> 12639 </div> 12640 </div> 12641 <div class="KT-rainbow-item"> 12642 <button type="button">Ke mana harus menghubungi jika membutuhkan bantuan transaksi atau kendala akun?</button> 12643 <div class="KT-rainbow-answer"> 12644 <p>Layanan Customer Service profesional BERLINTOTO bersiaga penuh selama 24 jam nonstop setiap hari melalui fitur Live Chat dan kontak resmi untuk membantu kendala Anda dengan cepat dan ramah.</p> 12645 </div> 12646 </div> 12647</div> 12648 12649<style> 12650.KT-faq-rainbow{ 12651 max-width: 760px; 12652 margin: 40px auto; 12653 padding: 30px; 12654 box-sizing: border-box; 12655 position: relative; 12656 overflow: hidden; 12657 border-radius: 32px; 12658 font-family: Arial, sans-serif; 12659 background: radial-gradient(circle at 15% 10%, rgba(177 2 2), transparent 18%), radial-gradient(circle at 85% 15%, rgb(207 163 3), transparent 25%), radial-gradient(circle at 20% 90%, rgb(207 163 3), transparent 25%), linear-gradient(135deg, #c70000, #8e0b0b, #960e0e, #dcce77); 12660 border: 2px solid rgba(177 2 2); 12661 box-shadow: 0 0 25px rgb(207 163 3), 0 0 55px rgba(196 199 0), inset 0 0 35px rgba(145 0 0); 12662} 12663 12664.KT-shine{ 12665 position:absolute; 12666 top:-90px; 12667 left:-120px; 12668 width:260px; 12669 height:520px; 12670 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12671 transform:rotate(25deg); 12672 animation:KTShine 4s infinite; 12673 pointer-events:none; 12674} 12675 12676@keyframes KTShine{ 12677 0%{left:-180px;} 12678 55%{left:110%;} 12679 100%{left:110%;} 12680} 12681 12682.KT-faq-top{ 12683 position:relative; 12684 text-align:center; 12685 margin-bottom:24px; 12686} 12687 12688.KT-faq-top span{ 12689 display:inline-block; 12690 padding:8px 22px; 12691 border-radius:999px; 12692 background:rgba(185 162 0 / 0%); 12693 color:#dbdbdb; 12694 font-size:12px; 12695 font-weight:900; 12696 letter-spacing:2px; 12697 border:1px solid rgba(215 215 215 / 62%); 12698 box-shadow:inset 0 0 14px rgba(255,255,255,.25); 12699} 12700 12701.KT-faq-top h2{ 12702 margin:16px 0 8px; 12703 color:#000000; 12704 font-size:34px; 12705 line-height:1.15; 12706 text-shadow: 12707 0 0 10px rgba(177 0 0), 12708 0 0 22px rgba(173 0 0); 12709} 12710 12711.KT-faq-top p{ 12712 margin:0; 12713 color:#f3ffff; 12714 font-size:14px; 12715} 12716 12717.KT-rainbow-item{ 12718 position:relative; 12719 overflow:hidden; 12720 margin-bottom:14px; 12721 border-radius:20px; 12722 background:rgba(255,255,255,.20); 12723 border:1px solid rgba(255,255,255,.48); 12724 box-shadow: 12725 inset 0 1px 0 rgba(255,255,255,.6), 12726 0 10px 25px rgba(0,0,0,.18); 12727 backdrop-filter:blur(10px); 12728} 12729 12730.KT-rainbow-item:before{ 12731 content:""; 12732 position:absolute; 12733 top:0; 12734 left:0; 12735 right:0; 12736 height:45%; 12737 background:linear-gradient(rgba(255,255,255,.32),rgba(255,255,255,.04)); 12738 pointer-events:none; 12739} 12740 12741.KT-rainbow-item button{ 12742 width:100%; 12743 padding:18px 58px 18px 20px; 12744 border:0; 12745 background: #af0000; 12746 color:#dbdbdb; 12747 text-align:left; 12748 cursor:pointer; 12749 font-size:15px; 12750 font-weight:900; 12751 position:relative; 12752 z-index:2; 12753 text-shadow:0 2px 6px rgba(0,0,0,.35); 12754} 12755 12756.KT-rainbow-item button:after{ 12757 content:"+"; 12758 position:absolute; 12759 right:18px; 12760 top:50%; 12761 transform:translateY(-50%); 12762 width:34px; 12763 height:34px; 12764 border-radius:50%; 12765 background:linear-gradient(135deg,#b10000, #9b0000); 12766 color:#d1d1d1; 12767 text-align:center; 12768 line-height:34px; 12769 font-size:24px; 12770 font-weight:900; 12771 box-shadow: 12772 0 0 14px rgba(255,255,255,.8), 12773 inset 0 -4px 8px rgba(0,0,0,.18); 12774} 12775 12776.KT-rainbow-item.active button:after{ 12777 content:"â"; 12778 background:linear-gradient(135deg,#ff0000, #5b0000); 12779} 12780 12781.KT-rainbow-answer{ 12782 max-height:0; 12783 overflow:hidden; 12784 transition:max-height .35s ease; 12785 position:relative; 12786 z-index:2; 12787} 12788 12789.KT-rainbow-item.active .KT-rainbow-answer{ 12790 max-height:170px; 12791} 12792 12793.KT-rainbow-answer p{ 12794 margin:0; 12795 padding:0 20px 18px; 12796 color:#dbdbdb; 12797 font-size:14px; 12798 line-height:1.7; 12799 text-shadow:0 2px 5px rgba(0 0 0); 12800} 12801 12802@media(max-width:600px){ 12803 .KT-faq-rainbow{ 12804 margin:25px 12px; 12805 padding:22px; 12806 border-radius:24px; 12807 } 12808 12809 .KT-faq-top h2{ 12810 font-size:26px; 12811 } 12812} 12813</style> 12814
12815<script> 12816document.querySelectorAll(".KT-rainbow-item button").forEach(function(btn){ 12817 btn.addEventListener("click", function(){ 12818 this.parentElement.classList.toggle("active"); 12819 }); 12820}); 12821</script>
12821 12822<style> 12823@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap'); 12824 12825.cyber-mecha { 12826 max-width: 800px; 12827 margin: 40px auto; 12828 12829 /* deep luxury red */ 12830 background: radial-gradient(circle at top, #14b1d7, #14b1d7 70%); 12831 12832 border-radius: 16px; 12833 border: 1px solid rgba(255, 90, 90, 0.35); 12834 12835 box-shadow: 12836 0 0 25px rgba(140, 30, 30, 0.35), 12837 inset 0 0 20px rgba(255, 60, 60, 0.10); 12838 12839 padding: 25px; 12840 font-family: 'Orbitron', sans-serif; 12841 color: #f7e6e6; 12842 position: relative; 12843 overflow: hidden; 12844} 12845 12846/* scanline overlay */ 12847.cyber-mecha::before { 12848 content: ""; 12849 position: absolute; 12850 inset: 0; 12851 background: repeating-linear-gradient( 12852 to bottom, 12853 rgba(255,90,90,0.03), 12854 rgba(255,90,90,0.03) 1px, 12855 transparent 1px, 12856 transparent 3px 12857 ); 12858 pointer-events: none; 12859} 12860 12861.pd-full-desc-title { 12862 text-align: center; 12863 font-size: 22px; 12864 12865 /* elegant muted red */ 12866 color: #e08b8b; 12867 12868 letter-spacing: 2px; 12869 text-shadow: 0 0 10px rgba(224, 139, 139, 0.4); 12870 margin-bottom: 20px; 12871} 12872 12873.mecha-header h3 { 12874 text-align: center; 12875 font-size: 18px; 12876 color: #ffb3b3; 12877 text-shadow: 0 0 8px rgba(255, 90, 90, 0.5); 12878 margin-bottom: 15px; 12879} 12880 12881.mecha-image img { 12882 width: 100%; 12883 border-radius: 10px; 12884 border: 1px solid rgba(255, 90, 90, 0.4); 12885 box-shadow: 0 0 20px rgba(255, 60, 60, 0.25); 12886 margin-bottom: 20px; 12887} 12888 12889.mecha-content p { 12890 font-size: 14px; 12891 line-height: 1.8; 12892 margin-bottom: 14px; 12893 color: #f3dede; 12894} 12895 12896.glow { 12897 color: #e08b8b; 12898 text-shadow: 0 0 6px rgba(224, 139, 139, 0.5); 12899 font-weight: 600; 12900} 12901 12902.glow-strong { 12903 color: #ff6b6b; 12904 text-shadow: 12905 0 0 10px rgba(255, 90, 90, 0.6), 12906 0 0 20px rgba(255, 60, 60, 0.4); 12907 font-weight: 700; 12908} 12909 12910.final-line { 12911 margin-top: 18px; 12912 padding: 10px; 12913 border: 1px solid rgba(255, 90, 90, 0.35); 12914 background: rgba(60, 0, 0, 0.25); 12915 border-radius: 8px; 12916 text-align: center; 12917 color: #ffd0d0; 12918} 12919</style> 12920 12921 12922 12923 <section class="pd-related-full"> 12924 <h2 class="pd-related-title">Produk Terkait</h2> 12925 <div class="pd-related-grid pd-related-grid--full"> 12926 <a class="pd-related-card" href="https://www.bim.miami/"> 12927 <div class="thumb"> 12928 <img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO"> 12929 <span class="mini-disc">-2%</span> </div> 12930 <div class="info"> 12931 <h3>BERLINTOTO</h3> 12932 <div class="spec-chips">I5 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX3050</div> <span class="price"> 12933 <span class="final">Rp 11.500.000</span> 12934 <span class="ori">Rp 12.799.000</span> 12935 </span> 12936 <span class="sold">Terjual 200+</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12937 </div> 12938 </a> 12939 <a class="pd-related-card" href="https://www.bim.miami/"> 12940 <div class="thumb"> 12941 <img src="https://www.bim.miami/berlintoto.png" alt="TOTO TOGEL"> 12942 <span class="mini-disc">-1%</span> </div> 12943 <div class="info"> 12944 <h3>BERLIN TOTO</h3> 12945 <div class="spec-chips">I7 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX5050</div> <span class="price"> 12946 <span class="final">Rp 22.887.000</span> 12947 <span class="ori">Rp 21.999.000</span> 12948 </span> 12949 <span class="sold">Terjual 20+</span> <span class="stock-dot out" title="Habis"></span> 12950 </div> 12951 </a> 12952 <a class="pd-related-card" href="https://www.bim.miami/"> 12953 <div class="thumb"> 12954 <img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO">
12955 <span class="mini-disc">-1%</span> </div> 12956 <div class="info"> 12957 <h3>LOGIN BERLINTOTO</h3> 12958 <div class="spec-chips">R7AI <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 1TB <span class="spec-divider">|</span> RTX5060</div> <span class="price"> 12959 <span class="final">Rp 23.600.000</span> 12960 <span class="ori">Rp 21.399.000</span> 12961 </span> 12962 <span class="sold">Terjual 6</span> <span class="stock-dot ready" title="Ready"></span> 12963 </div> 12964 </a> 12965 <a class="pd-related-card" href="https://www.bim.miami/"> 12966 <div class="thumb"> 12967 <img src="https://www.bim.miami/berlintoto.png" alt="TOGEL 4D"> 12968 <span class="mini-disc">-1%</span> </div> 12969 <div class="info"> 12970 <h3>DAFTAR BERLINTOTO</h3> 12971 <div class="spec-chips">R5AI <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 1TB <span class="spec-divider">|</span> RTX5060</div> <span class="price"> 12972 <span class="final">Rp 18.800.000</span> 12973 <span class="ori">Rp 19.799.000</span> 12974 </span> 12975 <span class="sold">Terjual 5</span> <span class="stock-dot ready" title="Ready"></span> 12976 </div> 12977 </a> 12978 <a class="pd-related-card" href="https://www.bim.miami/"> 12979 <div class="thumb"> 12980 <img src="https://www.bim.miami/berlintoto.png" alt="TOTO TOGEL 4D"> 12981 <span class="mini-disc">-1%</span> </div> 12982 <div class="info"> 12983 <h3>LINK BERLINTOTO</h3> 12984 <div class="spec-chips">I7 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX5050</div> <span class="price"> 12985 <span class="final">Rp 28.600.080</span> 12986 <span class="ori">Rp 27.999.000</span> 12987 </span> 12988 <span class="sold">Terjual 6</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12989 </div> 12990 </a> 12991 <a class="pd-related-card" href="https://www.bim.miami/"> 12992 <div class="thumb"> 12993 <img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO"> 12994 <span class="mini-disc">-1%</span> </div> 12995 <div class="info"> 12996 <h3>SLOT GACOR</h3> 12997 <div class="spec-chips">CORE5 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX4050</div> <span class="price"> 12998 <span class="final">Rp 17.888.000</span> 12999 <span class="ori">Rp 18.499.000</span> 13000 </span> 13001 <span class="stock-dot low" title="Stok Terbatas"></span> 13002 </div> 13003 </a> 13004 13005 </div> 13006 </section> 13007 13008 <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
13008-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></button><button type="button" class="mf-btn mf-chat" id="mf-chat" aria-label="Tanya Produk"><svg viewBox="0 0 640 640" fill="currentColor"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg></button><button type="button" class="mf-btn mf-cart" id="mf-cart" disabled aria-label="Tambah ke keranjang"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/></svg></button><button type="button" class="mf-btn mf-buy" id="mf-buy" disabled><span class="mf-buy-text">Beli Sekarang</span><span class="mf-buy-price">Rp 20.789.000</span></button></div></div> 13009 13010 <button type="button" 13011 class="pd-scroll-top" 13012 id="pd-scroll-top" 13013 aria-label="Kembali ke atas" 13014 title="Kembali ke atas"> 13015 <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"> 13016 <path fill="currentColor" d="M7.41 14.59 12 10l4.59 4.59L18 13.17l-6-6-6 6z"/> 13017 </svg> 13018 </button> 13019</main> 13020 13021<div class="pd-share-modal-overlay" id="pd-share-modal-overlay"><div class="pd-share-modal"><div class="pd-share-header"><h2 class="pd-share-title">Bagikan Produk</h2><button type="button" class="pd-share-close" id="pd-share-close" aria-label="Tutup"><svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button></div><div class="pd-share-body"><div class="pd-share-preview"><img src="https://www.bim.miami/berlintoto.png" alt="BERLINTOTO" class="pd-share-preview-img"><div class="pd-share-preview-info"><h3 class="pd-share-preview-name">BERLINTOTO</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">
13021<path fill="currentColor" d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg></span><span class="pd-share-name">Facebook</span></a><a href="#" class="pd-share-option" data-share="twitter" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-twitter"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></span><span class="pd-share-name">Twitter</span></a><a href="#" class="pd-share-option" data-share="telegram" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-telegram"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg></span><span class="pd-share-name">Telegram</span></a><a href="#" class="pd-share-option" data-share="email"><span class="pd-share-icon pd-share-icon-email"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg></span><span class="pd-share-name">Email</span></a><button type="button" class="pd-share-option" data-share="copy"><span class="pd-share-icon pd-share-icon-copy"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg></span><span class="pd-share-name">Salin Link</span></button><button type="button" class="pd-share-option" data-share="pdf"><span class="pd-share-icon pd-share-icon-pdf"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zM8 15.5h1.5c.83 0 1.5-.67 1.5-1.5v-1c0-.83-.67-1.5-1.5-1.5H8v4zm6 0h1c1.1 0 2-.9 2-2v-2c0-1.1-.9-2-2-2h-1v6zm-4.5-1.5H8v-1h1.5v1zM14 13h1v1h-1v-1z"/></svg></span><span class="pd-share-name">Export PDF</span></button></div><div class="pd-share-copy-section"><div class="pd-share-copy-input-wrap"><input type="text" class="pd-share-copy-input" id="pd-share-url-input" value="https://www.bim.miami/" 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> 13022 13023<style>.toast-root{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);display:flex;flex-direction:column;gap:10px;z-index:99999;pointer-events:none;max-width:min(92vw,540px)}.toast{display:grid;grid-template-columns:28px 1fr auto;gap:12px;background:#0b1220;color:#eef3ff;border:1px solid rgba(255,255,255,.08);padding:12px 14px;border-radius:12px;box-shadow:0 10px 30px rgba(10,10,20,.35);pointer-events:auto;overflow:hidden}.toast .ico{align-self:start;margin-top:2px}.toast .ttl{font-weight:700;line-height:1.25;font-size:14px;margin-bottom:4px}.toast .msg{opacity:.95;line-height:1.45;font-size:13px}.toast .actions{display:flex;gap:8px;align-self:center}.toast .btn{background:#0ea5e9;color:#fff;border:0;border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;cursor:pointer}.toast .btn.alt{background:transparent;color:#8fb5ff;border:1px solid rgba(143,181,255,.35)}.toast .close{background:transparent;border:0;color:#98a2b3;cursor:pointer;padding:4px;margin-left:6px}.toast.success{border-left:4px solid #22c55e}.toast.info{border-left:4px solid #0ea5e9}.toast.warn{border-left:4px solid #f59e0b}.toast .progress{position:absolute;
13023left:0;bottom:0;height:3px;background:linear-gradient(90deg,#60a5fa,#22c55e);width:100%;transform-origin:left;animation:toastBar linear forwards}@keyframes toastBar{from{transform:scaleX(1)}to{transform:scaleX(0)}}@media(max-width:640px){.toast{grid-template-columns:24px 1fr auto;padding:12px}.toast .btn{padding:7px 10px}}</style> 13024<style> 13025.toast .msg { word-break: break-word; overflow-wrap: anywhere; } 13026 13027.toast-root { bottom: max(12px, env(safe-area-inset-bottom, 12px)); } 13028 13029@media (max-width: 540px) { 13030 .toast-root { 13031 left: 0; right: 0; transform: none; 13032 padding: 0 12px; 13033 bottom: calc(max(12px, env(safe-area-inset-bottom, 12px)) + var(--fab-offset, 72px)); 13034 max-width: none; 13035 } 13036 .toast { 13037 grid-template-columns: 1fr; /* 1 kolom */ 13038 position: relative; 13039 padding: 14px 14px 12px 14px; 13040 border-radius: 16px; 13041 } 13042 .toast .ico { 13043 position: absolute; left: 14px; top: 14px; 13044 margin: 0; transform: translateY(1px); 13045 } 13046 .toast .cnt { padding-left: 34px; } 13047 .toast .ttl { font-size: 15px; margin-bottom: 6px; } 13048 .toast .msg { font-size: 13px; opacity: .96; } 13049 .toast .actions { 13050 margin-top: 12px; gap: 10px; width: 100%; 13051 display: grid; grid-template-columns: 1fr 1fr; 13052 } 13053 .toast .btn { width: 100%; min-width: 0; padding: 10px 0; } 13054 .toast .close { position: absolute; right: 6px; top: 6px; } 13055} 13056</style> 13057
13058<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13059(function adjustToastFabOffset(){ 13060 const root = document.getElementById('toastRoot'); 13061 if (!root) return; 13062 13063 function isVisible(el){ 13064 if (!el) return false; 13065 const cs = getComputedStyle(el); 13066 if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; 13067 const r = el.getBoundingClientRect(); 13068 return r.width > 0 && r.height > 0; 13069 } 13070 13071 function update(){ 13072 const fab = document.getElementById('pd-mobile-fab'); 13073 let extra = 16; 13074 if (isVisible(fab)) { 13075 const h = Math.round(fab.getBoundingClientRect().height) || 56; 13076 extra = h + 16; // FAB height + margin 13077 } 13078 root.style.setProperty('--fab-offset', extra + 'px'); 13079 } 13080 13081 window.addEventListener('resize', update, { passive: true }); 13082 const fab = document.getElementById('pd-mobile-fab'); 13083 if (window.ResizeObserver && fab) { 13084 const ro = new ResizeObserver(update); 13085 ro.observe(fab); 13086 } 13087 13088 update(); 13089})(); 13090</script>
13090 13091<div class="toast-root" id="toastRoot" aria-live="polite" aria-atomic="true"></div> 13092
13093<script nonce="se3iyzEHMc9FQEB4wSyd0w==">(function(){const root=document.getElementById('toastRoot');function svgIcon(kind){if(kind==='success')return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#22c55e"><path d="M9 16.2l-3.5-3.5 1.4-1.4L9 13.4l7.7-7.7 1.4 1.4z"/></svg>';if(kind==='warn')return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#f59e0b"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>';return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#0ea5e9"><path d="M11 7h2v2h-2zm0 4h2v6h-2z"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z" fill="none" stroke="#0ea5e9"/></svg>'} 13094window.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>`; 13095 13096 const close = ()=> { 13097 if (!el.parentNode) return; 13098 el.style.transition = 'transform .2s ease, opacity .2s ease'; 13099 el.style.opacity = '0'; el.style.transform = 'translateY(10px)'; 13100 setTimeout(()=>{ if (el.parentNode) el.parentNode.removeChild(el); }, 220); 13101 }; 13102 13103 el.querySelector('.close').addEventListener('click', close); 13104 el.querySelectorAll('.actions .btn').forEach(btn=>{ 13105 btn.addEventListener('click', ()=>{ 13106 const lab = btn.getAttribute('data-act'); 13107 try { 13108 (o.actions||[]).forEach(a=>{ if (a.label===lab && typeof a.onClick==='function') a.onClick(); }); 13109 } catch(_){} 13110 close(); 13111 }); 13112 }); 13113 13114 root.appendChild(el); 13115 setTimeout(close, o.duration); 13116 return el; 13117 }; 13118})(); 13119 13120const _0x1a2b=[{"url":"https://www.bim.miami/berlintoto.png","alt":"Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y","slug":""},{"url":"https://www.bim.miami/berlintoto.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"https://www.bim.miami/berlintoto.png"},{"url":"https://www.bim.miami/berlintoto.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp"},{"url":"https://www.bim.miami/berlintoto.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp"},{"url":"https://www.bim.miami/berlintoto.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp"},{"url":"https://www.bim.miami/berlintoto.png","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=>
13120c.textContent.trim()).filter(Boolean);return b.length?b.join(' / '):''} 13121function encodeVariantHash(id){if(!id||id<=0)return '';const xor=id^0x4A5D;return xor.toString(36)} 13122function decodeVariantHash(hash){if(!hash)return 0;const xor=parseInt(hash,36);return isNaN(xor)?0:xor^0x4A5D} 13123 13124function 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})} 13125 13126function executeBuyNowLegacy(id,variantId){const a=getVariantSummaryText(),b=(function(){const c=document.getElementById('pd-sku');return c?c.textContent.trim():_0x5e6f})(); 13127 13128 let willRedirect = false; 13129 try { 13130 if (window.addToCart) { 13131 window.addToCart(id, 1, variantId); 13132 willRedirect = false; 13133 } 13134 } catch(_){} 13135 13136 if (willRedirect) { 13137 showToast({ 13138 type: 'info', 13139 title: 'Menuju Keranjang', 13140 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13141 duration: 1800 13142 }); 13143 setTimeout(()=>{ window.location.href='/keranjang'; }, 1200); 13144 } else { 13145 showToast({ 13146 type: 'warn', 13147 title: 'Fitur Beli Langsung Segera Hadir', 13148 message: `Sementara ini Anda bisa menambahkan produk ke keranjang terlebih dahulu.<br><strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}`, 13149 duration: 6000, 13150 actions: [ 13151 { label: '+ Keranjang', onClick: ()=> { try{ if (window.addToCart) window.addToCart(id,1,variantId); }catch(_){} } }, 13152 { label: 'Hubungi CS', variant: 'alt', onClick: ()=> { window.open('https://wa.me/6281380009898','_blank'); } } 13153 ] 13154 }); 13155 } 13156} 13157 13158document.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)})}}); 13159 13160function _0x2m3n(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} 13161 13162(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')}})})(); 13163 13164(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;
13164slides.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})})(); 13165 13166/* ================= Simulasi Cicilan ================= */ 13167(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'} 13168 13169function _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'} 13170 13171card.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)}); 13172 13173window.instSetBasePrice=function(newPrice){setBasePrice(newPrice);_0x4o5p(activeBank())};_0x4o5p(activeBank()); 13174})(); 13175 13176/* ====== Fokus gambar varian ====== */ 13177function _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 ''}}} 13178function _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}} 13179 if (c){ 13180 const targetName = _0x8s9t(url); 13181 const slideImg = Array.from(c.querySelectorAll('img')).find(im => _0x8s9t(im.src) === targetName); 13182 if (slideImg){ 13183 13184 try { 13185 const container = c; 13186 const imgRect = slideImg.getBoundingClientRect(); 13187 const contRect = container.getBoundingClientRect(); 13188 const offsetLeft = slideImg.offsetLeft - container.offsetLeft; 13189 const center = offsetLeft - (container.clientWidth - slideImg.clientWidth) / 2; 13190 container.scrollTo({ left: Math.max(0, center), behavior: 'smooth' }); 13191 } catch(_){ 13192 13193 slideImg.scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); 13194 } 13195 } 13196 } 13197} 13198function findImageForVariant(variant){ 13199 if (!variant || !variant.attrs) return null; 13200 const tokens = []; 13201 try{ 13202 Object.values(variant.attrs).forEach(v=>{ 13203 if (!v) return; 13204 const raw = String(v); 13205 tokens.push(raw); 13206 tokens.push(normalize(raw)); 13207 }); 13208 }catch(_){} 13209 if (variant.sku){ 13210 tokens.push(String(variant.sku)); 13211 tokens.push(normalize(String(variant.sku))); 13212 } 13213 if (variant.image_url) { 13214 tokens.push(String(variant.image_url)); 13215 } 13216 let best = null; 13217 let bestScore = -1; 13218 13219 for (const g of (galleryData||[])){ 13220 const slugN = normalize(g.slug||''); 13221 const altN = normalize(g.alt||''); 13222 const file = _0x8s9t(g.url||''); 13223 const fileN = normalize(file); 13224 const imgU = g.url; 13225 13226 for (const t of tokens){ 13227 const tN = normalize(t); 13228 if (!tN) continue; 13229 13230 let score = -1; 13231 if (imgU === tN) score = 300; 13232 if (slugN && slugN === tN) score = 300; 13233 else if (fileN && fileN.includes(tN)) score = 200; 13234 else if (altN && altN.includes(tN)) score = 120; 13235 13236 if (score > bestScore){ 13237 bestScore = score; 13238 best = g; 13239 } 13240 if (bestScore >= 300) break; 13241 } 13242 if (bestScore >= 300) break; 13243 } 13244 return bestScore >= 120 ? best : null; 13245} 13246function _0xey3z(variant){const a=_0xcw2x(variant);if(a&&a.url){_0xau1v(a.url,a.alt||'')}} 13247
13248/* ====== Layout mobile ====== */ 13249(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; 13250 13251 let pricePlaced = false; 13252 let varPlaced = false; 13253 let titlePlaced = false; 13254 let promoPlaced = false; 13255 const mq = window.matchMedia('(max-width: 640px)'); 13256 13257 function apply(){ 13258 if (mq.matches) { 13259 if (!titlePlaced && a && titleRow && d.contains(titleRow)) { try{a.appendChild(titleRow);titlePlaced=true;}catch(_){} } 13260 if (!pricePlaced && a && b) { try{a.appendChild(b);pricePlaced=true;}catch(_){} } 13261 if (!varPlaced && f && d && e) { 13262 try{ 13263 f.hidden = false; 13264 if (i && !f.contains(i)) f.appendChild(i); 13265 if (g && !f.contains(g)) f.appendChild(g); 13266 if(d.contains(e))d.insertBefore(f, e); 13267 varPlaced = true; 13268 }catch(_){} 13269 } 13270 if (!promoPlaced && promoBanner && compareSection && compareSection.parentNode) { 13271 try{ 13272 compareSection.parentNode.insertBefore(promoBanner, compareSection); 13273 promoPlaced = true; 13274 }catch(_){} 13275 } 13276 } else { 13277 if (titlePlaced && titleRow && d) { 13278 try{ 13279 const vcombined = document.getElementById('pd-variant-combined'); 13280 if (vcombined && d.contains(vcombined)) { 13281 d.insertBefore(titleRow, vcombined); 13282 } else { 13283 d.insertBefore(titleRow, d.firstChild); 13284 } 13285 titlePlaced=false; 13286 }catch(_){} 13287 } 13288 if (pricePlaced && b && c && c.parentNode) { 13289 try{c.parentNode.insertBefore(b, c.nextSibling);pricePlaced=false;}catch(_){} 13290 } 13291 if (varPlaced) { 13292 try{ 13293 if (i && j && j.parentNode && !j.parentNode.contains(i)) j.parentNode.insertBefore(i, j.nextSibling); 13294 if (g && h && h.parentNode && !h.parentNode.contains(g)) h.parentNode.insertBefore(g, h.nextSibling); 13295 if (f) { f.hidden = true; f.innerHTML = ''; } 13296 varPlaced = false; 13297 }catch(_){} 13298 } 13299 if (promoPlaced && promoBanner && b) { 13300 try{ 13301 b.appendChild(promoBanner); 13302 promoPlaced = false; 13303 }catch(_){} 13304 } 13305 } 13306 } 13307 13308 mq.addEventListener ? mq.addEventListener('change', apply) : mq.addListener(apply); 13309 apply(); 13310 13311 const fab = document.getElementById('pd-mobile-fab'); 13312 if (fab) { 13313 const pid = parseInt(fab.getAttribute('data-pid')||'0',10) || 325; 13314 const btnCart = document.getElementById('mf-cart'); 13315 13316 if (btnCart) btnCart.addEventListener('click', function(){ 13317 const varInput = document.getElementById('pd-variant-id'); 13318 const variantId = varInput ? parseInt(varInput.value||'0',10) : 0; 13319 try { if (window.addToCart) addToCart(pid, 1, variantId); } catch(_){} 13320 const variantText = getVariantSummaryText(); 13321 const skuText = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim():PD_SKU; })(); 13322 showToast({ 13323 type: 'success', 13324 title: 'Berhasil ditambahkan ke Keranjang', 13325 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13326 duration: 5000, 13327 actions: [ 13328 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 13329 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 13330 ] 13331 }); 13332 }); 13333 } 13334})(); 13335 13336/* ====== Auto-hide installment banner if sidebar too tall (Desktop only) ====== */ 13337(function(){ 13338 function adjustSidebarBanner() { 13339 if (window.innerWidth <= 768) return; 13340 const sidebar = document.querySelector('.pd-sidebar'); 13341 const infoCard = document.querySelector('.pd-info-card'); 13342 const banner = document.querySelector('.pd-side-card.inst-alt-banner.inst-alt-banner--compact'); 13343 if (!sidebar || !infoCard || !banner) return; 13344 const sidebarHeight = sidebar.offsetHeight; 13345 const infoCardHeight = infoCard.offsetHeight; 13346 if (sidebarHeight > infoCardHeight) { 13347 banner.style.display = 'none'; 13348 } else { 13349 banner.style.display = ''; 13350 } 13351 } 13352 if (document.readyState === 'loading') { 13353 document.addEventListener('DOMContentLoaded', adjustSidebarBanner); 13354 } else { 13355 adjustSidebarBanner(); 13356 } 13357 let resizeTimer; 13358 window.addEventListener('resize', function() { 13359 clearTimeout(resizeTimer); 13360 resizeTimer = setTimeout(adjustSidebarBanner, 250); 13361 }); 13362 const pwpSection = document.getElementById('pwp-deals-section'); 13363 if (pwpSection) { 13364 const observer = new MutationObserver(function(mutations) {
13365 let shouldRecheck = false; 13366 mutations.forEach(function(mutation) { 13367 if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') { 13368 shouldRecheck = true; 13369 } else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 13370 shouldRecheck = true; 13371 } 13372 }); 13373 if (shouldRecheck) { 13374 setTimeout(adjustSidebarBanner, 100); 13375 } 13376 }); 13377 observer.observe(pwpSection, { 13378 attributes: true, 13379 childList: true, 13380 subtree: true 13381 }); 13382 } 13383 window.adjustSidebarBanner = adjustSidebarBanner; 13384})(); 13385 13386/* ====== Global constants ====== */ 13387const IS_DIGITAL=false; 13388const IS_PREORDER=false; 13389 13390/* ====== Variants state (REVISED) ====== */ 13391(function initVariants(){ 13392const _0x7g8h=[];window.variants=_0x7g8h; 13393window.isPreorderProduct=false; 13394 const attrOrder = []; 13395 13396 if (!variants.length || !attrOrder.length) return; 13397 13398 function comboKey(sel){ 13399 return attrOrder.map(a => a + '=' + (sel[a] ?? '')).join('|'); 13400 } 13401 13402 const variantMap = {};
13403 variants.forEach(v=>{ 13404 const key = comboKey(v.attrs); 13405 variantMap[key] = v; 13406 }); 13407 13408 function readSelection(){ 13409 const sel = {}; 13410 attrOrder.forEach(a=>{ 13411 const r = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio]:checked`); 13412 sel[a] = r ? r.value : ''; 13413 }); 13414 return sel; 13415 } 13416 13417 function findVariant(sel){ 13418 return variantMap[comboKey(sel)] || null; 13419 } 13420 13421 function autoFillMissing(sel){ 13422 let changed = false; 13423 for (const a of attrOrder) { 13424 if (sel[a]) continue; 13425 const possible = []; 13426 const seen = new Set(); 13427 variants.forEach(v=>{ 13428 let ok = true; 13429 for (const a2 of attrOrder) { 13430 if (a2 === a) continue; 13431 if (sel[a2] && v.attrs[a2] !== sel[a2]) { ok = false; break; } 13432 } 13433 if (ok) { 13434 const val = v.attrs[a]; 13435 if (!seen.has(val)) { seen.add(val); possible.push(val); } 13436 } 13437 }); 13438 if (possible.length === 1) { 13439 const val = possible[0]; 13440 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13441 if (input && !input.checked) { input.checked = true; sel[a] = val; changed = true; } 13442 } 13443 } 13444 return changed; 13445 } 13446 13447 function rupiah(n){ 13448 n = parseInt(n||0,10)||0; 13449 return 'Rp ' + n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.'); 13450 } 13451 13452 function focusGalleryByUrl(imageUrl) { 13453 const isMobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; 13454 if (isMobile) { 13455 _0xau1v(imageUrl); 13456 return; 13457 } 13458 13459 const gallery = document.getElementById('pd-thumb-gallery'); 13460 const targetName = _0x8s9t(imageUrl); 13461 13462 if (gallery) { 13463 const btns = Array.from(gallery.querySelectorAll('.pd-thumb-btn')); 13464 const btn = btns.find(b => _0x8s9t(b.getAttribute('data-full') || '') === targetName); 13465 if (btn) { btn.click(); return; } 13466 } 13467 13468 _0xau1v(imageUrl); 13469 } 13470 13471 13472function formatStockLabel(n){n=parseInt(n||0,10)||0;return(n>10)?'10+':String(n)} 13473function 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(_){}} 13474 13475 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)} 13476 13477 function applyVariant(v, focusImage = true){ 13478 const elCur = document.getElementById('pd-price-current'); 13479 const elOri = document.getElementById('pd-price-original'); 13480 const elDisc = document.getElementById('pd-discount-badge'); 13481 const elSku = document.getElementById('pd-sku'); 13482 const elStock= document.getElementById('pd-stock-badge'); 13483 const elQty = document.getElementById('qty'); 13484 const btnAdd = document.getElementById('btn-add-cart'); 13485 const btnBuy = document.getElementById('btn-buy'); 13486 const hidVar = document.getElementById('pd-variant-id'); 13487 13488 const mfCart= document.getElementById('mf-cart'); 13489 const mfBuy = document.getElementById('mf-buy'); 13490 const fab = document.getElementById('pd-mobile-fab'); 13491 13492 if (hidVar) hidVar.value = v ? v.id : 0; 13493 if (elCur) elCur.textContent = rupiah(v ? v.final : 0); 13494 if (elSku && v && v.sku) elSku.textContent = v.sku; 13495 13496 if (v && v.ori > v.final) { 13497 if (elOri) { elOri.style.display='inline'; elOri.textContent = rupiah(v.ori); } 13498 if (elDisc) { elDisc.style.display='inline'; elDisc.textContent = '-' + (v.disc||0) + '%'; } 13499 } else { 13500 if (elOri) elOri.style.display='none'; 13501 if (elDisc) elDisc.style.display='none'; 13502 } 13503 13504 const stock = v ? (parseInt(v.stock||0,10)||0) : 0; 13505 let effectiveStock = stock; 13506 let isActivePreorder = false;
13507 if (window.isPreorderProduct && v && 'preorder_remaining' in v && v.preorder_remaining !== null) { 13508 effectiveStock = parseInt(v.preorder_remaining, 10); 13509 if (isNaN(effectiveStock)) effectiveStock = 0; 13510 isActivePreorder = true; 13511 } 13512 if (elStock){ 13513 elStock.classList.remove('in','out'); 13514 if (effectiveStock>0){ 13515 elStock.classList.add('in'); 13516 elStock.textContent = IS_DIGITAL ? 'Tersedia' : (isActivePreorder ? 'Sisa Slot: ' + effectiveStock : 'Stok: ' + formatStockLabel(effectiveStock)); 13517 } else { 13518 elStock.classList.add('out'); 13519 elStock.textContent = 'Habis'; 13520 } 13521 } 13522 if (elQty){ 13523 elQty.max = String(effectiveStock>0?effectiveStock:0); 13524 if (effectiveStock<=0) { elQty.value = '0'; elQty.disabled = true; } 13525 else { if (parseInt(elQty.value||'1',10) < 1) elQty.value='1'; elQty.disabled=false; } 13526 } 13527 13528 const shouldDisable = !(v && effectiveStock>0); 13529 const updateButtons = () => { 13530 const btnAddCart = document.getElementById('btn-add-cart'); 13531 const btnBuyNow = document.getElementById('btn-buy'); 13532 const mobileCart = document.getElementById('mf-cart'); 13533 const mobileBuy = document.getElementById('mf-buy'); 13534 [btnAddCart, btnBuyNow, mobileCart, mobileBuy].forEach(btn => { 13535 if (!btn) return; 13536 if (shouldDisable) { 13537 btn.disabled = true; 13538 btn.setAttribute('disabled', 'disabled'); 13539 btn.classList.add('stock-disabled'); 13540 btn.setAttribute('data-stock-blocked', 'true'); 13541 btn.setAttribute('data-stock-reason', window.isPreorderProduct ? 'Pre-order habis' : 'Stok habis'); 13542 } else { 13543 btn.disabled = false; 13544 btn.removeAttribute('disabled'); 13545 btn.classList.remove('stock-disabled'); 13546 btn.removeAttribute('data-stock-blocked'); 13547 btn.removeAttribute('data-stock-reason'); 13548 } 13549 }); 13550 }; 13551 updateButtons(); 13552 setTimeout(updateButtons, 10); 13553 setTimeout(updateButtons, 50); 13554 setTimeout(updateButtons, 100); 13555 if (mfBuy && v) { 13556 const voucherBtn = document.getElementById('btn-buy-voucher'); 13557 const voucherCode = fab ? fab.dataset.voucherCode : ''; 13558 let voucherDiscount = 0; 13559 13560 if (voucherBtn && voucherCode) { 13561 const vType = voucherBtn.dataset.voucherType; 13562 const vValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 13563 const vMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 13564 const finalPrice = v.final || 0; 13565 if (vType && vValue > 0) { 13566 voucherDiscount = calcVoucherDisc(vType, vValue, finalPrice, vMax); 13567 if (fab) fab.dataset.voucherValue = voucherDiscount.toString(); 13568 } 13569 } 13570 13571 const finalPrice = v.final || 0; 13572 const textEl = mfBuy.querySelector('.mf-buy-text'); 13573 const priceEl = mfBuy.querySelector('.mf-buy-price'); 13574 13575 if (effectiveStock > 0) { 13576 if (IS_PREORDER) { 13577 if (textEl) textEl.textContent = 'Pre Order Sekarang'; 13578 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13579 } else if (voucherCode && voucherDiscount > 0) { 13580 if (textEl) textEl.textContent = 'Beli dengan Voucher'; 13581 if (priceEl) priceEl.textContent = rupiah(Math.max(0, finalPrice - voucherDiscount)); 13582 } else { 13583 if (textEl) textEl.textContent = 'Beli Sekarang'; 13584 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13585 } 13586 } else { 13587 if (textEl) textEl.textContent = 'Stok Habis'; 13588 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13589 } 13590 } 13591 13592 const isMobile = window.initMobile ?? 0; 13593 if (window.instSetBasePrice) window.instSetBasePrice(v ? (isMobile == '1' ? v.ori : v.final) : 0); 13594 if (window.updateSoldBySku) window.updateSoldBySku(v.sku); 13595 updateBranchPopover(v); 13596 if (v && v.image_url && focusImage) focusGalleryByUrl(v.image_url); 13597 if (v) { 13598 window.dispatchEvent(new CustomEvent('pd-variant-changed', { 13599 detail: { 13600 price: v.final,
13601 originalPrice: v.ori, 13602 discount: v.disc, 13603 stock: effectiveStock, 13604 sku: v.sku, 13605 variant_id: v.id 13606 } 13607 })); 13608 13609 const encodedHash = encodeVariantHash(v.id); 13610 if (encodedHash && window.history && window.history.replaceState) { 13611 const newHash = '#v=' + encodedHash; 13612 const newUrl = window.location.pathname + window.location.search + newHash; 13613 window.history.replaceState(null, '', newUrl); 13614 } 13615 } 13616 } 13617 13618 let sel = readSelection(); 13619 if (autoFillMissing(sel)) sel = readSelection(); 13620 13621 let v = findVariant(sel); 13622 if (!v) { 13623 const candidates = variants.filter(variant=>{ 13624 return attrOrder.every(a=>{ 13625 if (!sel[a]) return true; 13626 return variant.attrs[a] === sel[a]; 13627 }); 13628 }); 13629 if (candidates.length >= 1) { 13630 v = candidates[0]; 13631 attrOrder.forEach(a=> sel[a] = v.attrs[a]); 13632 attrOrder.forEach(a=>{ 13633 const val = sel[a]; 13634 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13635 if (input) input.checked = true; 13636 }); 13637 } 13638 } 13639 13640 const initSel = readSelection(); 13641 const initVar = findVariant(initSel) || variants[0] || null; 13642 if (document.readyState === 'loading') { 13643 document.addEventListener('DOMContentLoaded', () => applyVariant(initVar, false)); 13644 } else { 13645 applyVariant(initVar, false); 13646 } 13647 13648function updateSummary(sel){Object.entries(sel).forEach(([a,val])=>{const el=document.getElementById('vs-'+a);if(el)el.textContent=val||'-'})} 13649 function refreshOptions(sel){ 13650 attrOrder.forEach(a=>{ 13651 const group = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"]`); 13652 if (!group) return; 13653 const chips = group.querySelectorAll('.v-chip'); 13654 chips.forEach(ch=>{ 13655 const val = ch.getAttribute('data-value'); 13656 const possible = variants.some(v=>{ 13657 if (v.attrs[a] !== val) return false; 13658 for (const a2 of attrOrder) { 13659 if (a2 === a) continue; 13660 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13661 } 13662 return true; 13663 }); 13664 const input = ch.querySelector('input[type=radio]'); 13665 const disabled = !possible; 13666 if (input) input.disabled = disabled; 13667 ch.classList.toggle('disabled', disabled); 13668 13669 let candidateVariant = null; 13670 if (possible) { 13671 candidateVariant = variants.find(v=>{ 13672 if (v.attrs[a] !== val) return false; 13673 for (const a2 of attrOrder) { 13674 if (a2 === a) continue; 13675 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13676 } 13677 return true; 13678 }); 13679 } 13680 const outOfStock = candidateVariant ? (window.isPreorderProduct && candidateVariant.preorder_remaining !== null ? candidateVariant.preorder_remaining <= 0 : candidateVariant.stock <= 0) : false; 13681 ch.classList.toggle('out', !disabled && outOfStock); 13682 const badge = ch.querySelector('.chip-badge'); 13683 if (badge) badge.hidden = !(candidateVariant && outOfStock); 13684 }); 13685 group.querySelectorAll('.v-chip').forEach(ch=>{ 13686 const input = ch.querySelector('input[type=radio]'); 13687 ch.classList.toggle('selected', input && input.checked); 13688 }); 13689 }); 13690 } 13691 updateSummary(sel); 13692 refreshOptions(sel); 13693 document.querySelectorAll('.v-chip').forEach(chip=>{const input=chip.querySelector('input[type=radio]');if(input&&input.checked){chip.classList.add('selected');}}); 13694 13695 const container = document.getElementById('pd-variants'); 13696 container && container.addEventListener('change', function(ev){ 13697 const radio = ev.target.closest('input[type=radio]'); 13698 if (!radio) return; 13699 sel = readSelection(); 13700 if (autoFillMissing(sel)) sel = readSelection(); 13701 const chosen = findVariant(sel); 13702 applyVariant(chosen || null, true); 13703 13704 updateSummary(sel); 13705 refreshOptions(sel); 13706 }); 13707 13708window.applyVariant=applyVariant; 13709})(); 13710 13711(function cleanBrokenDescImages(){ 13712 const containers = [ 13713 document.querySelector('.pd-full-desc-body'), 13714 document.querySelector('.pd-short-desc')
13715 ].filter(Boolean); 13716 13717 containers.forEach(root => { 13718 const imgs = root.querySelectorAll('img'); 13719 imgs.forEach(img => { 13720 if (img.complete && img.naturalWidth === 0) { 13721 img.remove(); 13722 return; 13723 } 13724 img.addEventListener('error', () => { 13725 img.remove(); 13726 }, { once:true }); 13727 }); 13728 }); 13729})(); 13730 13731/* ====== Scroll To Top ====== */ 13732(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'})})})(); 13733 13734/* Format stok 10+ */ 13735function _0xk67l(n){n=parseInt(n||0,10)||0;return n>10?'10+':String(n)} 13736 13737/* Toggle popover stok per cabang */ 13738(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=''}} 13739function 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()} 13740a.addEventListener('click',function(h){h.preventDefault();b.classList.contains('show')?f():e()});if(c)c.addEventListener('click',f)})(); 13741 13742/* ===== Share Product Modal ===== */ 13743(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://www.bim.miami/",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
13743ncodeURIComponent(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)}})(); 13744 13745/* ===== Embedding variants (server â client) ===== */ 13746(function(){ 13747 window.__PD_VARIANTS_WITH_BRANCH__ = []; 13748})(); 13749 13750/* ===== Helper: update angka popover ===== */ 13751function updateStockPopoverForVariant(v){ 13752 try{ 13753 const elMeg = document.getElementById('sp-qty-meg'); 13754 const elMdf = document.getElementById('sp-qty-mdf'); 13755 const elBda = document.getElementById('sp-qty-bda'); 13756 13757 if (typeof v?.smm === 'number' && elMeg) elMeg.textContent = formatStockLabel(Math.max(0, v.smm|0)); 13758 if (typeof v?.smf === 'number' && elMdf) elMdf.textContent = formatStockLabel(Math.max(0, v.smf|0)); 13759 if (typeof v?.sba === 'number' && elBda) elBda.textContent = formatStockLabel(Math.max(0, v.sba|0)); 13760 }catch(_){} 13761} 13762 13763/* ===== applyVariant: 10+ dan update popover ===== */ 13764(function patchApplyVariant(){ 13765 if (typeof applyVariant !== 'function') return; 13766 const original = applyVariant; 13767 window.applyVariant = function(v){ 13768 original(v); 13769 13770 try { 13771 const elStock = document.getElementById('pd-stock-badge'); 13772 if (elStock){ 13773 let txt = elStock.textContent || ''; 13774 let m = txt.match(/\d+/); 13775 if (m) { 13776 const n = parseInt(m[0]||'0',10)||0; 13777 elStock.textContent = (n>0) ? ('Stok: ' + formatStockLabel(n)) : 'Habis'; 13778 } 13779 } 13780 } catch(_){} 13781if(window.__PD_VARIANTS_WITH_BRANCH__&&v){const vv=window.__PD_VARIANTS_WITH_BRANCH__.find(x=>x.id===v.id)||v;updateStockPopoverForVariant(vv)} 13782 }; 13783})(); 13784 13785(function initCurrentVariantBranch(){ 13786 const list = window.__PD_VARIANTS_WITH_BRANCH__ || []; 13787 if (!list.length) return; 13788 const hid = document.getElementById('pd-variant-id'); 13789 const vid = hid ? parseInt(hid.value||'0',10) : 0; 13790 const current = list.find(x => x.id === vid) || list[0] || null; 13791 if (current) updateStockPopoverForVariant(current); 13792})(); 13793 13794(function(){ 13795 if (!window.ga4Ecomm) return; 13796 const catSegs = ["Komputer & Laptop","Laptop","Gaming"]; 13797 window.ga4Ecomm.viewItem({ 13798 currency: 'IDR', 13799 value: 20789000, 13800 item_id: "SB0B3T", 13801 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", 13802 item_brand: "Lenovo ",
13803 item_category: catSegs && catSegs.length ? catSegs.join(' / ') : '' 13804 }); 13805 })(); 13806 13807(function patchApplyVariantFocus(){ 13808 if (typeof applyVariant !== 'function') return; 13809 const original = applyVariant; 13810 window.applyVariant = function(v, focusImage = true){ 13811 13812 original(v, focusImage); 13813 13814 if (focusImage && v && v.image_url) { 13815 _0xau1v(v.image_url); 13816 } 13817 }; 13818})(); 13819 13820(function() { 13821 var soldMap = []; 13822 var soldTotalLabel = "Terjual 2"; 13823 13824 function formatSoldLabel(qty) { 13825 if (qty >= 1000000) { 13826 var v = Math.floor((qty / 100000) ) / 10; 13827 return 'Terjual ' + (String(v).replace('.', ',')) + 'jt+'; 13828 } 13829 if (qty >= 1000) { 13830 var v2 = Math.floor((qty / 100) ) / 10; 13831 return 'Terjual ' + (String(v2).replace('.', ',')) + 'rb+'; 13832 } 13833 if (qty >= 100) { 13834 var v2 = Math.floor((qty / 100) ) * 100; 13835 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13836 } 13837 if (qty >= 10) { 13838 var v2 = Math.floor((qty / 10) ) * 10; 13839 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13840 } 13841 return 'Terjual ' + (qty.toLocaleString('id-ID')); 13842 } 13843 13844 function updateSoldLabel(labelText) { 13845 var el = document.querySelector('#pd-sold-inline, [data-role="sold-chip"]'); 13846 if (el) el.textContent = labelText; 13847 } 13848 13849 window.updateSoldBySku = function(sku) { 13850 if (!sku || typeof sku !== 'string') { 13851 updateSoldLabel(soldTotalLabel); 13852 return; 13853 } 13854 var qty = soldMap[sku] || 0; 13855 updateSoldLabel(formatSoldLabel(qty)); 13856 }; 13857 13858 updateSoldLabel(soldTotalLabel); 13859 })(); 13860 13861(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)}})} 13862function 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} 13863const 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}});
13863function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}})(); 13864 13865(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')}})(); 13866</script>
vendor: 1 bytes, line 13866
13866
13867<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13868window.PD_ID=325;window.PD_NAME=_0x3c4d;window.PD_SKU=_0x5e6f;window.PD_CATEGORY_PATH="Komputer & Laptop/Laptop/Gaming";window.PD_BEST_VOUCHER=null; 13869window.IS_DIGITAL=IS_DIGITAL; 13870window.isPreorderProduct=false; 13871window.PD_PREORDER_DATA=null; 13872</script>
13872 13873
13874<script data-inlined="pd-lightbox.js"> 13875// PlazaIT Product Lightbox (vanilla, no CDN) â v4 (swipe-down to close) 13876(function(){ 13877 const state = { 13878 items: [], 13879 open: false, 13880 idx: 0, 13881 scale: 1, 13882 minScale: 1, 13883 maxScale: 4, 13884 tx: 0, ty: 0, 13885 isDragging: false, 13886 dragStartX: 0, dragStartY: 0, 13887 baseTx: 0, baseTy: 0, 13888 touchStartX: 0, touchStartY: 0, 13889 lastTap: 0, 13890 swipeMode: 'none', // 'none' | 'h' | 'v' 13891 totalDx: 0, totalDy: 0 13892 }; 13893 13894 let root, imgEl, counterEl, btnPrev, btnNext, btnClose, canvas; 13895 13896 const ORIGIN = (location.origin || (location.protocol + '//' + location.host)); 13897 13898 function absUrl(u){ 13899 if (!u) return ''; 13900 try { 13901 if (/^https?:\/\//i.test(u)) return u; 13902 return new URL(u, ORIGIN).href; 13903 } catch(_) { return u; } 13904 } 13905 function fileName(u){ 13906 try { return new URL(u, ORIGIN).pathname.split('/').pop().toLowerCase(); } 13907 catch(_){ return String(u||'').split('/').pop().toLowerCase(); } 13908 } 13909 13910 function buildItems(){ 13911 const gd = (window.galleryData || []).filter(Boolean); 13912 if (gd.length){ 13913 state.items = gd.map(g => ({ 13914 src: absUrl(g.url), alt: g.alt || '', slug: g.slug || '', w: g.w || 1200, h: g.h || 1200 13915 })).filter(it => !!it.src); 13916 return; 13917 } 13918 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 13919 state.items = Array.from(thumbs).map(btn => { 13920 const raw = btn.getAttribute('data-full') || ''; 13921 return { src: absUrl(raw), alt: btn.getAttribute('data-alt') || '', slug: btn.getAttribute('data-slug') || '', w: 1200, h: 1200 }; 13922 }).filter(it => !!it.src); 13923 } 13924 13925 function ensureUI(){ 13926 if (root) return; 13927 root = document.createElement('div'); 13928 root.className = 'pd-lb'; 13929 root.id = 'pd-lightbox'; 13930 root.setAttribute('role','dialog'); 13931 root.setAttribute('aria-modal','true'); 13932 root.setAttribute('aria-hidden','true'); 13933 root.setAttribute('tabindex','-1'); 13934 13935 // Canvas terlebih dulu (z-index di CSS membuat tombol di atas) 13936 root.innerHTML = ` 13937 <div class="pd-lb-canvas"><img class="pd-lb-img" id="pd-lb-img" alt=""></div> 13938 <button class="pd-lb-close" aria-label="Tutup" title="Tutup">×</button> 13939 <button class="pd-lb-prev" aria-label="Sebelumnya" title="Sebelumnya">❮</button> 13940 <button class="pd-lb-next" aria-label="Berikutnya" title="Berikutnya">❯</button> 13941 <div class="pd-lb-counter" id="pd-lb-counter">1 / 1</div> 13942 `; 13943 document.body.appendChild(root); 13944 13945 imgEl = root.querySelector('#pd-lb-img'); 13946 counterEl = root.querySelector('#pd-lb-counter'); 13947 btnPrev = root.querySelector('.pd-lb-prev'); 13948 btnNext = root.querySelector('.pd-lb-next'); 13949 btnClose = root.querySelector('.pd-lb-close'); 13950 canvas = root.querySelector('.pd-lb-canvas'); 13951 13952 // Button clicks 13953 btnPrev.addEventListener('click', (e)=>{ e.stopPropagation(); prev(); }); 13954 btnNext.addEventListener('click', (e)=>{ e.stopPropagation(); next(); }); 13955 btnClose.addEventListener('click', (e)=>{ e.stopPropagation(); close(); }); 13956 13957 // Klik backdrop untuk tutup 13958 root.addEventListener('click', (e)=>{ if (e.target === root) close(); }); 13959 13960 // Keyboard global 13961 document.addEventListener('keydown', (e)=>{ 13962 if (!state.open) return; 13963 if (e.key === 'Escape') { e.preventDefault(); close(); } 13964 if (e.key === 'ArrowRight') { e.preventDefault(); next(); } 13965 if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } 13966 }); 13967 13968 // Wheel zoom (desktop) 13969 root.addEventListener('wheel', (e)=>{ 13970 if (!state.open) return; 13971 e.preventDefault(); 13972 const delta = -Math.sign(e.deltaY) * 0.2; 13973 zoomAt(delta, e.clientX, e.clientY); 13974 }, { passive: false }); 13975 13976 // Drag pan (mouse) 13977 canvas.addEventListener('mousedown', (e)=>{ 13978 if (state.scale <= 1) return; 13979 state.isDragging = true; 13980 state.dragStartX = e.clientX; 13981 state.dragStartY = e.clientY; 13982 state.baseTx = state.tx; 13983 state.baseTy = state.ty; 13984 e.preventDefault(); 13985 }); 13986 window.addEventListener('mousemove', (e)=>{ 13987 if (!state.isDragging) return; 13988 const dx = e.clientX - state.dragStartX; 13989 const dy = e.clientY - state.dragStartY; 13990 setTranslate(state.baseTx + dx, state.baseTy + dy); 13991 }); 13992 window.addEventListener('mouseup', ()=>{ state.isDragging = false; }); 13993 13994 // Touch: swipe navigate (H), swipe-down close (V), double-tap zoom, drag pan 13995 canvas.addEventListener('touchstart', (e)=>{
13996 if (!state.open) return; 13997 if (e.touches.length === 1) { 13998 const t = e.touches[0]; 13999 state.touchStartX = t.clientX; 14000 state.touchStartY = t.clientY; 14001 state.totalDx = 0; 14002 state.totalDy = 0; 14003 state.swipeMode = 'none'; 14004 14005 state.isDragging = state.scale > 1; 14006 state.dragStartX = t.clientX; 14007 state.dragStartY = t.clientY; 14008 state.baseTx = state.tx; 14009 state.baseTy = state.ty; 14010 14011 // Double-tap toggle zoom 14012 const now = Date.now(); 14013 if (now - state.lastTap < 300) { 14014 if (state.scale > 1) resetTransform(); else zoomAt(+1, t.clientX, t.clientY, 2); 14015 state.lastTap = 0; 14016 e.preventDefault(); 14017 return; 14018 } 14019 state.lastTap = now; 14020 } 14021 }, { passive: true }); 14022 14023 canvas.addEventListener('touchmove', (e)=>{ 14024 if (!state.open) return; 14025 14026 // Drag pan saat zoom 14027 if (state.isDragging && e.touches.length === 1 && state.scale > 1) { 14028 e.preventDefault(); 14029 const t = e.touches[0]; 14030 const dx = t.clientX - state.dragStartX; 14031 const dy = t.clientY - state.dragStartY; 14032 setTranslate(state.baseTx + dx, state.baseTy + dy); 14033 return; 14034 } 14035 14036 // Gesture saat scale <= 1 14037 if (state.scale <= 1 && e.touches.length === 1) { 14038 const t = e.touches[0]; 14039 const dx = t.clientX - state.touchStartX; 14040 const dy = t.clientY - state.touchStartY; 14041 state.totalDx = dx; state.totalDy = dy; 14042 14043 // Tentukan mode swipe di awal gerakan 14044 if (state.swipeMode === 'none') { 14045 const absX = Math.abs(dx), absY = Math.abs(dy); 14046 if (absY > 12 && absY > absX * 1.3) { 14047 state.swipeMode = 'v'; // swipe vertical (untuk close) 14048 } else if (absX > 12 && absX > absY * 1.3) { 14049 state.swipeMode = 'h'; // swipe horizontal (ganti slide) 14050 } 14051 } 14052 14053 if (state.swipeMode === 'v') { 14054 // Ikuti jari, kurangi opacity untuk feedback 14055 e.preventDefault(); 14056 setRootDrag(dy); 14057 } else if (state.swipeMode === 'h') { 14058 // Biarkan sampai touchend untuk menentukan next/prev (tidak perlu preventDefault di sini) 14059 } 14060 } 14061 }, { passive: false }); 14062 14063 canvas.addEventListener('touchend', (e)=>{ 14064 if (!state.open) return; 14065 14066 if (state.scale <= 1) { 14067 const dx = state.totalDx; 14068 const dy = state.totalDy; 14069 14070 if (state.swipeMode === 'v') { 14071 const TH = 80; // threshold jarak piksel untuk menutup 14072 if (Math.abs(dy) > TH) { 14073 // Animasi keluar arah swipe 14074 animateClose(dy); 14075 } else { 14076 // Snap back 14077 resetRootDrag(true); 14078 } 14079 } else if (state.swipeMode === 'h') { 14080 if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); } 14081 } 14082 } 14083 14084 state.isDragging = false; 14085 state.swipeMode = 'none'; 14086 state.totalDx = 0; 14087 state.totalDy = 0; 14088 }); 14089 } 14090 14091 function setImage(idx){ 14092 if (!state.items.length) return; 14093 state.idx = (idx + state.items.length) % state.items.length; 14094 const it = state.items[state.idx]; 14095 imgEl.src = it.src; 14096 imgEl.alt = it.alt || ''; 14097 counterEl.textContent = (state.idx + 1) + ' / ' + state.items.length; 14098 resetTransform(); 14099 preloadNeighbors(); 14100 } 14101 14102 function resetTransform(){ 14103 state.scale = 1; 14104 state.tx = 0; state.ty = 0; 14105 applyTransform(); 14106 } 14107 14108 function setTranslate(tx, ty){ 14109 state.tx = tx; state.ty = ty; 14110 applyTransform(); 14111 } 14112 14113 function applyTransform(){ 14114 imgEl.style.transform = `translate3d(${state.tx}px, ${state.ty}px, 0) scale(${state.scale})`; 14115 } 14116 14117 function zoomAt(delta, clientX, clientY, snapTo){ 14118 const old = state.scale; 14119 let next = (typeof snapTo === 'number') ? snapTo : (old + delta); 14120 next = Math.max(state.minScale, Math.min(state.maxScale, next)); 14121 14122 if (next !== old) { 14123 const rect = imgEl.getBoundingClientRect(); 14124 const cx = clientX - rect.left - rect.width/2; 14125 const cy = clientY - rect.top - rect.height/2; 14126 state.tx -= cx * (next/old - 1); 14127 state.ty -= cy * (next/old - 1); 14128 state.scale = next; 14129 applyTransform(); 14130 } 14131 } 14132 14133 function preloadNeighbors(){ 14134 if (state.items.length < 2) return; 14135 const nextIdx = (state.idx + 1) % state.items.length; 14136 const prevIdx = (state.idx - 1 + state.items.length) % state.items.length;
14137 [nextIdx, prevIdx].forEach(i=>{ 14138 const img = new Image(); img.src = state.items[i].src; 14139 }); 14140 } 14141 14142 // ----- Swipe-down visual helpers ----- 14143 function setRootDrag(dy){ 14144 const damped = dy * 0.6; // redam gerakan agar lembut 14145 const abs = Math.abs(dy); 14146 const fade = Math.max(0, Math.min(1, 1 - abs / 300)); // transparansi sesuai jarak 14147 root.style.transform = `translate3d(0, ${damped}px, 0)`; 14148 root.style.opacity = String(0.95 * fade + 0.05); // jangan jadi 0 total sebelum close 14149 } 14150 function resetRootDrag(withAnim){ 14151 if (withAnim) { 14152 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14153 root.style.transform = 'translate3d(0,0,0)'; 14154 root.style.opacity = '1'; 14155 setTimeout(()=>{ root.style.transition = ''; }, 190); 14156 } else { 14157 root.style.transition = ''; 14158 root.style.transform = ''; 14159 root.style.opacity = ''; 14160 } 14161 } 14162 function animateClose(dy){ 14163 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14164 const off = (dy > 0 ? window.innerHeight : -window.innerHeight); 14165 root.style.transform = `translate3d(0, ${off}px, 0)`; 14166 root.style.opacity = '0'; 14167 setTimeout(()=>{ root.style.transition = ''; close(); }, 180); 14168 } 14169 14170 function setSiblingsInert(makeInert){ 14171 // Inert semua saudara root (elemen lain di body) agar fokus tidak lari 14172 const nodes = Array.from(document.body.children).filter(el => el !== root); 14173 nodes.forEach(el=>{ 14174 if (makeInert) { 14175 if (!el.hasAttribute('inert')) el.setAttribute('inert',''); 14176 } else { 14177 if (el.hasAttribute('inert')) el.removeAttribute('inert'); 14178 } 14179 }); 14180 } 14181 14182 function open(idx){ 14183 if (!state.items.length) buildItems(); 14184 if (!state.items.length) return; 14185 ensureUI(); 14186 14187 // Reset efek drag visual 14188 resetRootDrag(false); 14189 14190 document.body.style.overflow = 'hidden'; 14191 root.classList.add('show'); 14192 root.removeAttribute('aria-hidden'); // hindari aria-hidden saat fokus child 14193 setSiblingsInert(true); 14194 14195 state.open = true; 14196 setImage(typeof idx === 'number' ? idx : 0); 14197 14198 // Fokuskan dialog agar keyboard pasti tertangkap 14199 try { root.focus(); } catch(_){} 14200 } 14201 14202 function close(){ 14203 state.open = false; 14204 root.classList.remove('show'); 14205 root.setAttribute('aria-hidden','true'); 14206 setSiblingsInert(false); 14207 document.body.style.overflow = ''; 14208 14209 // Bersihkan transform/opacity 14210 resetRootDrag(false); 14211 } 14212 14213 function next(){ setImage(state.idx + 1); } 14214 function prev(){ setImage(state.idx - 1); } 14215 14216 function findIndexByUrl(url){ 14217 const target = fileName(absUrl(url)); 14218 const idx = state.items.findIndex(it => fileName(it.src) === target); 14219 return idx >= 0 ? idx : 0; 14220 } 14221 14222 function initPdLightbox(){ 14223 buildItems(); 14224 ensureUI(); 14225 14226 // Klik gambar utama â buka LB di index yang sama 14227 const mainImg = document.getElementById('pd-main-img'); 14228 if (mainImg) { 14229 mainImg.style.cursor = 'zoom-in'; 14230 mainImg.addEventListener('click', ()=>{ 14231 open(findIndexByUrl(mainImg.src)); 14232 }); 14233 } 14234 14235 // Klik gambar slider mobile â buka LB di gambar yang diketuk 14236 const slider = document.getElementById('pd-mobile-slider'); 14237 if (slider) { 14238 slider.addEventListener('click', (e)=>{ 14239 const im = e.target && e.target.tagName === 'IMG' ? e.target : e.target.closest('img'); 14240 if (!im) return; 14241 open(findIndexByUrl(im.src)); 14242 }); 14243 } 14244 14245 // (Opsional) double-click pada thumbnail untuk buka langsung 14246 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 14247 thumbs.forEach((btn)=>{ 14248 let last = 0; 14249 btn.addEventListener('click', ()=>{ 14250 const now = Date.now(); 14251 if (now - last < 300) { 14252 const src = btn.getAttribute('data-full') || ''; 14253 open(findIndexByUrl(absUrl(src))); 14254 last = 0; 14255 } else { 14256 last = now; 14257 } 14258 }); 14259 }); 14260 14261 // Ekspos API 14262 window.PDLightbox = { open, close, next, prev, findIndexByUrl }; 14263 } 14264 14265 if (document.readyState === 'loading') { 14266 document.addEventListener('DOMContentLoaded', initPdLightbox); 14267 } else { 14268 initPdLightbox(); 14269 } 14270})(); 14271</script>
vendor: 1 bytes, line 14271
14271
14272<script data-inlined="cart.js"> 14273(function(){ 14274 function $(s,ctx=document){ return ctx.querySelector(s); } 14275 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 14276 14277 // CSRF token helper 14278 function getCsrfToken() { 14279 const metaTag = document.querySelector('meta[name="csrf-token"]'); 14280 return metaTag ? metaTag.getAttribute('content') : ''; 14281 } 14282 14283 function guessPdMeta(){ 14284 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 14285 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 14286 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 14287 const imgEl = $('#pd-main-img'); 14288 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 14289 return { name, sku, image_url }; // price removed for security 14290 } 14291 14292 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 14293 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 14294 const payload = { 14295 product_id: parseInt(productId||0,10) || 0, 14296 qty: parseInt(qty||1,10) || 1, 14297 variant_id: parseInt(variantId||0,10) || 0, 14298 meta 14299 }; 14300 14301 // Check if user is logged in 14302 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 14303 document.body.classList.contains('logged-in') || 14304 window.userLoggedIn === true || 14305 document.body.dataset.userLoggedIn === 'true'; 14306 14307 // Skip localStorage untuk logged user - mereka pakai database 14308 if (!isLoggedIn && window.guestCart) { 14309 try { 14310 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 14311 } catch (e) { 14312 console.warn('Failed to add to localStorage cart:', e); 14313 } 14314 } 14315 14316 const res = await fetch('/api/cart/add.php', { 14317 method: 'POST', 14318 headers: { 14319 'Content-Type':'application/json', 14320 'X-CSRF-Token': getCsrfToken() 14321 }, 14322 credentials: 'same-origin', 14323 body: JSON.stringify({ 14324 ...payload, 14325 csrf_token: getCsrfToken() 14326 }), 14327 }); 14328 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 14329 if (!res.ok || !data.ok) { 14330 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 14331 throw new Error(msg); 14332 } 14333 14334 // Update badge di navbar dengan data dari response 14335 try { 14336 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14337 if (el && data.cart && typeof data.cart.items_count === 'number') { 14338 el.textContent = String(data.cart.items_count); 14339 el.classList.toggle('is-empty', data.cart.items_count <= 0); 14340 } 14341 } catch(_){} 14342 14343 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 14344 try { 14345 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 14346 } catch(_){} 14347 14348 // Update CartBadge dengan data dari response (lebih reliable) 14349 try { 14350 if (window.CartBadge && window.CartBadge.set) { 14351 window.CartBadge.set(data.cart.items_count||0); 14352 } 14353 } catch(_){} 14354 14355 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 14356 setTimeout(() => { 14357 if (window.updateCartCounter) { 14358 window.updateCartCounter(); 14359 } 14360 }, 100); 14361 14362 return data; 14363 } 14364 14365 // Function to update cart counter from server 14366 async function updateCartCounter(){ 14367 try { 14368 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 14369 const data = await res.json().catch(()=>({ok:false})); 14370 if (data.ok && typeof data.items_count === 'number') { 14371 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14372 if (el) { 14373 el.textContent = String(data.items_count); 14374 el.classList.toggle('is-empty', data.items_count <= 0); 14375 } 14376 // Dispatch event 14377 try { 14378 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 14379 } catch(_){} 14380 } 14381 } catch (err) { 14382 console.log('Could not update cart counter:', err); 14383 } 14384 } 14385 14386 // Function to manually merge guest cart (if needed) 14387 async function mergeGuestCart(){ 14388 try { 14389 const res = await fetch('/api/cart/merge-guest.php', { 14390 method: 'POST', 14391 headers: { 14392 'Content-Type': 'application/json', 14393 'X-CSRF-Token': getCsrfToken() 14394 }, 14395 credentials: 'same-origin', 14396 body: JSON.stringify({ csrf_token: getCsrfToken() }) 14397 }); 14398 const data = await res.json().catch(()=>({ok:false})); 14399 14400 if (data.ok && data.merged_items > 0) { 14401 console.log(`Guest cart merged: ${data.merged_items} items`); 14402 // Update cart counter after merge 14403 await updateCartCounter(); 14404 return data; 14405 } 14406 return data; 14407 } catch (err) { 14408 console.log('Could not merge guest cart:', err); 14409 return { ok: false, error: err.message }; 14410 } 14411 } 14412 14413 window.addToCart = addToCart; 14414 window.updateCartCounter = updateCartCounter; 14415 window.mergeGuestCart = mergeGuestCart; 14416 14417 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 14418 document.addEventListener('click', async (e)=>{ 14419 const btn = e.target.closest('[data-addtocart]'); 14420 if (!btn) return; 14421 e.preventDefault(); 14422 if (btn.disabled) return; 14423 14424 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 14425 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
14426 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 14427 14428 try { 14429 const data = await addToCart(pid, qty, vid); 14430 if (window.showToast) { 14431 const meta = guessPdMeta(); 14432 window.showToast({ 14433 type:'success', 14434 title:'Berhasil ditambahkan', 14435 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 14436 duration: 3800, 14437 actions: [ 14438 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 14439 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 14440 ] 14441 }); 14442 } 14443 } catch (err) { 14444 if (window.showToast) { 14445 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 14446 } else { 14447 console.error(err); 14448 } 14449 } 14450 }); 14451})(); 14452</script>
vendor: 1 bytes, line 14452
14452
14453<script data-inlined="stock-validator-vanilla.js"> 14454/** 14455 * Stock Validation - Vanilla JS Version 14456 * 14457 * This script provides client-side stock validation without jQuery dependency 14458 * - Works with native JavaScript and fetch API 14459 * - Graceful fallback for older browsers 14460 * - Same functionality as jQuery version 14461 */ 14462 14463window.StockValidator = (function() { 14464 'use strict'; 14465 14466 const config = { 14467 apiEndpoint: '/api/stock/check.php', 14468 modalSelector: '#stock-modal-overlay', // We create our own modal 14469 loadingClass: 'loading-stock', 14470 disabledClass: 'stock-disabled', 14471 debug: true // Enable debug logging 14472 }; 14473 14474 /** 14475 * Cross-browser event listener helper 14476 */ 14477 function addEventListener(element, event, handler) { 14478 if (element.addEventListener) { 14479 element.addEventListener(event, handler); 14480 } else if (element.attachEvent) { 14481 element.attachEvent('on' + event, handler); 14482 } 14483 } 14484 14485 /** 14486 * Get stock information from embedded variant data (instant, no AJAX needed) 14487 */ 14488 function getStockFromEmbeddedData(variantId) { 14489 // Check if embedded variant data exists 14490 if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { 14491 return null; 14492 } 14493 14494 const variants = window.__PD_VARIANTS_WITH_BRANCH__; 14495 14496 // SPECIAL CASE: Simple product (no variants) - empty array means simple product 14497 if (variants.length === 0) { 14498 // For simple products, we cannot determine stock from embedded data 14499 // Return null to force API validation 14500 return null; 14501 } 14502 14503 // If no variant specified, check base product stock (all variants) 14504 if (!variantId || variantId <= 0) { 14505 // For base product, check if ANY variant has stock 14506 const isPreorder = window.isPreorderProduct || false; 14507 let hasAnyStock = false; 14508 let totalStock = 0; 14509 14510 if (isPreorder) { 14511 // For pre-order, check preorder_remaining 14512 hasAnyStock = variants.some(v => { 14513 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14514 return v.preorder_remaining > 0; 14515 } 14516 return v.stock > 0; 14517 }); 14518 totalStock = variants.reduce((sum, v) => { 14519 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14520 return sum + (v.preorder_remaining || 0); 14521 } 14522 return sum + (v.stock || 0); 14523 }, 0); 14524 } else { 14525 hasAnyStock = variants.some(v => v.stock > 0); 14526 totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); 14527 } 14528 14529 return { 14530 hasStock: hasAnyStock, 14531 stock: totalStock, 14532 sku: 'Base Product', 14533 variant: null 14534 }; 14535 } 14536 14537 // Find specific variant 14538 const variant = variants.find(v => v.id === variantId); 14539 14540 if (!variant) { 14541 return { 14542 hasStock: false, 14543 stock: 0, 14544 sku: 'Unknown Variant', 14545 variant: null 14546 }; 14547 } 14548 14549 // For pre-order products, check preorder_remaining instead of stock 14550 const isPreorder = window.isPreorderProduct || false; 14551 let hasStock = false; 14552 let availableStock = 0; 14553 14554 if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { 14555 availableStock = parseInt(variant.preorder_remaining, 10); 14556 if (isNaN(availableStock)) availableStock = 0; 14557 hasStock = availableStock > 0; 14558 } else { 14559 availableStock = variant.stock || 0; 14560 hasStock = availableStock > 0; 14561 } 14562 14563 return { 14564 hasStock: hasStock, 14565 stock: availableStock, 14566 sku: variant.sku, 14567 variant: variant 14568 }; 14569 } 14570 14571 /** 14572 * Cross-browser AJAX helper 14573 */
14574 function makeRequest(url, options) { 14575 return new Promise((resolve, reject) => { 14576 // Try fetch first (modern browsers) 14577 if (typeof fetch !== 'undefined') { 14578 const fetchOptions = { 14579 method: options.method || 'POST', 14580 headers: { 14581 'Content-Type': 'application/x-www-form-urlencoded', 14582 }, 14583 body: options.data ? new URLSearchParams(options.data).toString() : null 14584 }; 14585 14586 fetch(url, fetchOptions) 14587 .then(response => { 14588 if (!response.ok) { 14589 throw new Error(`HTTP ${response.status}`); 14590 } 14591 return response.json(); 14592 }) 14593 .then(resolve) 14594 .catch(reject); 14595 } else { 14596 // Fallback to XMLHttpRequest 14597 const xhr = new XMLHttpRequest(); 14598 xhr.open(options.method || 'POST', url); 14599 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 14600 14601 xhr.onload = function() { 14602 if (xhr.status >= 200 && xhr.status < 300) { 14603 try { 14604 resolve(JSON.parse(xhr.responseText)); 14605 } catch (e) { 14606 reject(new Error('Invalid JSON response')); 14607 } 14608 } else { 14609 reject(new Error(`HTTP ${xhr.status}`)); 14610 } 14611 }; 14612 14613 xhr.onerror = function() { 14614 reject(new Error('Network error')); 14615 }; 14616 14617 const data = options.data ? new URLSearchParams(options.data).toString() : null; 14618 xhr.send(data); 14619 } 14620 }); 14621 } 14622 14623 /** 14624 * Check stock via AJAX 14625 */ 14626 function checkStock(productId, variantId, qty) { 14627 return makeRequest(config.apiEndpoint, { 14628 method: 'POST', 14629 data: { 14630 product_id: productId, 14631 variant_id: variantId || 0, 14632 qty: qty || 1 14633 } 14634 }); 14635 } 14636 14637 /** 14638 * Create and show stock alert modal using modal.css structure 14639 */ 14640 function showStockModal(stockInfo, action = 'add to cart') { 14641 let title, message, type, iconClass; 14642 14643 // Handle both string and object formats 14644 if (typeof stockInfo === 'string') { 14645 title = 'Stok Tidak Tersedia'; 14646 message = stockInfo; 14647 type = 'warning'; 14648 iconClass = 'warning'; 14649 } else if (stockInfo && typeof stockInfo === 'object') { 14650 if (!stockInfo.is_active) { 14651 title = 'Produk Tidak Tersedia'; 14652 message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; 14653 type = 'danger'; 14654 iconClass = 'danger'; 14655 } else if (!stockInfo.is_in_stock) { 14656 title = 'Stok Habis'; 14657 message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; 14658 if (stockInfo.sku) { 14659 message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`; 14660 } 14661 type = 'warning'; 14662 iconClass = 'warning'; 14663 } else if (!stockInfo.can_add_to_cart) { 14664 title = 'Stok Tidak Mencukupi'; 14665 message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`; 14666 message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`; 14667 message += `Silakan kurangi jumlah atau pilih varian lain.`; 14668 type = 'warning'; 14669 iconClass = 'warning'; 14670 } else { 14671 title = 'Stok Tidak Tersedia'; 14672 message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14673 type = 'warning'; 14674 iconClass = 'warning'; 14675 } 14676 } else { 14677 title = 'Stok Tidak Tersedia'; 14678 message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14679 type = 'warning'; 14680 iconClass = 'warning'; 14681 } 14682 14683 // Create modal HTML using modal.css structure 14684 const modalHTML = ` 14685 <div class="modal-overlay" id="stock-modal-overlay"> 14686 <div class="modal-dialog"> 14687 <div class="modal-header"> 14688 <div class="modal-icon ${iconClass}"> 14689 ${getIconSVG(iconClass)} 14690 </div> 14691 <h3 class="modal-title">${title}</h3> 14692 </div> 14693 <div class="modal-body"> 14694 <p class="modal-message">${message}</p> 14695 <div class="modal-actions"> 14696 <button type="button" class="modal-btn modal-btn-secondary" data-action="close"> 14697 Tutup 14698 </button> 14699 <button type="button" class="modal-btn modal-btn-primary" data-action="reload"> 14700 Muat Ulang 14701 </button> 14702 </div> 14703 </div> 14704 </div> 14705 </div> 14706 `; 14707 14708 // Remove existing modal if any 14709 const existingModal = document.getElementById('stock-modal-overlay'); 14710 if (existingModal) { 14711 existingModal.remove(); 14712 } 14713 14714 // Add modal to body 14715 document.body.insertAdjacentHTML('beforeend', modalHTML); 14716 14717 // Show modal with animation 14718 const modal = document.getElementById('stock-modal-overlay'); 14719 setTimeout(() => { 14720 modal.classList.add('show'); 14721 }, 10); 14722 14723 // Add event listeners to modal buttons 14724 const closeBtn = modal.querySelector('[data-action="close"]'); 14725 const reloadBtn = modal.querySelector('[data-action="reload"]'); 14726 14727 if (closeBtn) { 14728 addEventListener(closeBtn, 'click', closeStockModal); 14729 } 14730 14731 if (reloadBtn) { 14732 addEventListener(reloadBtn, 'click', function() { 14733 closeStockModal(); 14734 window.location.reload(); 14735 }); 14736 } 14737 14738 // Close modal when clicking overlay 14739 addEventListener(modal, 'click', function(event) { 14740 if (event.target === modal) { 14741 closeStockModal(); 14742 } 14743 }); 14744 14745 // Auto-close after 8 seconds 14746 setTimeout(() => { 14747 closeStockModal(); 14748 }, 8000); 14749 } 14750 14751 /** 14752 * Get icon SVG based on type 14753 */ 14754 function getIconSVG(type) { 14755 const icons = { 14756 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>', 14757 warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
14757<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>', 14758 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>', 14759 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>' 14760 }; 14761 return icons[type] || icons.info; 14762 } 14763 14764 /** 14765 * Close stock modal 14766 */ 14767 function closeStockModal() { 14768 const modal = document.getElementById('stock-modal-overlay'); 14769 if (modal) { 14770 modal.classList.remove('show'); 14771 setTimeout(() => { 14772 modal.remove(); 14773 }, 200); 14774 } 14775 } 14776 14777 // Make closeStockModal globally available 14778 if (typeof window !== 'undefined') { 14779 window.closeStockModal = closeStockModal; 14780 } 14781 14782 /** 14783 * Get element data attribute with fallback 14784 */ 14785 function getDataAttribute(element, attr) { 14786 if (element.dataset) { 14787 return element.dataset[attr]; 14788 } else { 14789 return element.getAttribute('data-' + attr); 14790 } 14791 } 14792 14793 /** 14794 * Validate stock before cart action 14795 */ 14796 async function validateBeforeCart(productId, variantId, qty, action = 'add') { 14797 try { 14798 // SMART VALIDATION: For simple products, check if we already have cached stock info 14799 // If buttons are enabled and cached info shows stock available, skip API validation 14800 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { 14801 14802 return true; 14803 } 14804 14805 // For variant products or when cached info is not available, do API validation 14806 const stockInfo = await checkStock(productId, variantId, qty); 14807 14808 if (!stockInfo.ok) { 14809 showStockModal({ 14810 message: stockInfo.error || 'Gagal mengecek stok', 14811 is_active: false 14812 }, action); 14813 return false; 14814 } 14815 14816 if (!stockInfo.can_add_to_cart) { 14817 showStockModal(stockInfo, action); 14818 return false; 14819 } 14820 14821 return true; // Stock available, proceed 14822 14823 } catch (error) { 14824 console.error('Stock validation error:', error); 14825 showStockModal({ 14826 message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', 14827 is_active: false 14828 }, action); 14829 return false; 14830 } 14831 } 14832 14833 /** 14834 * Handle button click with stock validation 14835 */ 14836 function handleButtonClick(event, action) { 14837 const button = event.target; 14838 14839 14840 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14841 if (button.hasAttribute('data-stock-blocked')) { 14842 event.preventDefault(); 14843 event.stopImmediatePropagation(); 14844 14845 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14846 showStockModal(reason); 14847 14848 // Show security warning 14849 if (window.showToast) { 14850 window.showToast({ 14851 type: 'warning', 14852 title: 'Aksi Tidak Diizinkan', 14853 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14854 duration: 3000 14855 }); 14856 } 14857 return false; 14858 } 14859 14860 // SECOND CHECK: Real-time stock validation against cached stock info 14861 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14862 event.preventDefault(); 14863 event.stopImmediatePropagation(); 14864 14865 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14866 14867 // Re-enforce button disabling 14868 button.disabled = true; 14869 button.style.opacity = '0.5'; 14870 button.style.cursor = 'not-allowed'; 14871 button.setAttribute('data-stock-blocked', 'true'); 14872 14873 return false;
14874 } 14875 14876 // Skip if already processing 14877 if (button.classList.contains(config.loadingClass)) { 14878 event.preventDefault(); 14879 return false; 14880 } 14881 14882 // Get product data from button attributes or form 14883 let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); 14884 let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); 14885 let qty = parseInt(getDataAttribute(button, 'qty') || 1); 14886 14887 // Try to get from global variables (set by product detail page) 14888 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14889 productId = parseInt(window.PD_ID || 0); 14890 } 14891 14892 // Try to get from parent form if not in button 14893 if (productId <= 0) { 14894 const form = button.closest ? button.closest('form') : null; 14895 if (form) { 14896 const pidAttr = form.getAttribute('data-pid'); 14897 const pidInput = form.querySelector('[name="product_id"]'); 14898 const vidInput = form.querySelector('[name="variant_id"]'); 14899 const qtyInput = form.querySelector('[name="qty"]'); 14900 14901 if (pidAttr) productId = parseInt(pidAttr); 14902 if (pidInput) productId = parseInt(pidInput.value || 0); 14903 if (vidInput) variantId = parseInt(vidInput.value || 0); 14904 if (qtyInput) qty = parseInt(qtyInput.value || 1); 14905 } 14906 } 14907 14908 // CRITICAL: Always get current variant from selector (overrides button attributes) 14909 // This ensures we validate against the currently selected variant, not cached button data 14910 const variantSelector = document.getElementById('pd-variant-id'); 14911 if (variantSelector) { 14912 const currentVariantId = parseInt(variantSelector.value || 0); 14913 if (currentVariantId > 0) { 14914 variantId = currentVariantId; 14915 } 14916 } 14917 14918 14919 if (productId <= 0) { 14920 return true; // Let default handler deal with it 14921 } 14922 14923 // Prevent default and validate stock 14924 event.preventDefault(); 14925 event.stopPropagation(); 14926 14927 // Show loading state 14928 button.classList.add(config.loadingClass); 14929 const originalDisabled = button.disabled; 14930 button.disabled = true; 14931 14932 14933 validateBeforeCart(productId, variantId, qty, action) 14934 .then(isValid => { 14935 if (isValid) { 14936 // Stock OK, proceed with original action 14937 if (action === 'cart' && typeof window.addToCart === 'function') { 14938 window.addToCart(productId, qty, variantId); 14939 } else if (action === 'buy' && typeof window.buyNow === 'function') { 14940 // Call buyNow from pd-buynow.js without parameters (it gets data internally) 14941 window.buyNow(); 14942 } else { 14943 // Add fallback cart/buy logic here if needed 14944 } 14945 } 14946 }) 14947 .catch(error => { 14948 }) 14949 .finally(() => { 14950 // Remove loading state 14951 button.classList.remove(config.loadingClass); 14952 button.disabled = originalDisabled; 14953 }); 14954 14955 return false; 14956 } 14957 14958 /** 14959 * Handle mobile button click with stock validation 14960 */ 14961 function handleMobileButtonClick(event, action) { 14962 const button = event.target; 14963 14964 14965 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14966 if (button.hasAttribute('data-stock-blocked')) { 14967 event.preventDefault(); 14968 event.stopImmediatePropagation(); 14969 14970 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14971 showStockModal(reason); 14972 14973 // Show security warning 14974 if (window.showToast) { 14975 window.showToast({ 14976 type: 'warning', 14977 title: 'Aksi Tidak Diizinkan', 14978 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14979 duration: 3000 14980 }); 14981 } 14982 return false;
14983 } 14984 14985 // SECOND CHECK: Real-time stock validation against cached stock info 14986 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14987 event.preventDefault(); 14988 event.stopImmediatePropagation(); 14989 14990 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14991 14992 // Re-enforce button disabling 14993 button.disabled = true; 14994 button.style.opacity = '0.5'; 14995 button.style.cursor = 'not-allowed'; 14996 button.setAttribute('data-stock-blocked', 'true'); 14997 14998 return false; 14999 } 15000 15001 // Skip if already processing 15002 if (button.classList.contains(config.loadingClass)) { 15003 event.preventDefault(); 15004 return false; 15005 } 15006 15007 // Get product data from mobile FAB container 15008 let productId = 0; 15009 let variantId = 0; 15010 let qty = 1; 15011 15012 // Try to get from mobile FAB data-pid 15013 const mobileFab = document.getElementById('pd-mobile-fab'); 15014 if (mobileFab) { 15015 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15016 if (pidAttr) productId = parseInt(pidAttr); 15017 } 15018 15019 // Try to get from global variables (set by product detail page) 15020 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 15021 productId = parseInt(window.PD_ID || 0); 15022 } 15023 15024 // CRITICAL: Always get current variant from selector (real-time) 15025 // This ensures we validate against the currently selected variant 15026 const variantSelector = document.getElementById('pd-variant-id'); 15027 if (variantSelector) { 15028 variantId = parseInt(variantSelector.value || 0); 15029 } 15030 15031 // Try to get quantity from quantity input 15032 const qtySelector = document.querySelector('[name="qty"]'); 15033 if (qtySelector) { 15034 qty = parseInt(qtySelector.value || 1); 15035 } 15036 15037 // Ensure minimum quantity is 1 15038 qty = Math.max(1, qty); 15039 15040 15041 if (productId <= 0) { 15042 return true; // Let default handler deal with it 15043 } 15044 15045 // Event already prevented at interceptor level, just proceed with validation 15046 15047 // Show loading state 15048 button.classList.add(config.loadingClass); 15049 const originalDisabled = button.disabled; 15050 const originalText = button.textContent; 15051 button.disabled = true; 15052 button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; 15053 15054 15055 // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions 15056 if (window.addToCart && typeof window.addToCart === 'function') { 15057 window.addToCart._stockValidatorBlocked = true; 15058 } 15059 15060 // Also preemptively block other cart functions 15061 const originalAddToCartSubmit = window.addToCartSubmit; 15062 const originalPdAddToCartSubmit = window._pdAddToCartSubmit; 15063 15064 // Temporarily block ALL cart functions during validation 15065 window.addToCartSubmit = (...args) => { 15066 return Promise.resolve(false); 15067 }; 15068 15069 if (originalPdAddToCartSubmit) { 15070 window._pdAddToCartSubmit = (...args) => { 15071 return Promise.resolve(false); 15072 }; 15073 } 15074 15075 validateBeforeCart(productId, variantId, qty, action) 15076 .then(isValid => { 15077 if (isValid) { 15078 // Stock OK, proceed with original action 15079 15080 if (action === 'cart') { 15081 // For mobile cart, we need to recreate the original behavior 15082 // Strategy: Try multiple methods to ensure cart add + toast success 15083 15084 15085 let cartAddSuccess = false; 15086 let errorMessage = ''; 15087 15088 // Method 1: Try original addToCart function 15089 const originalAddToCart = window.addToCart && window.addToCart._original; 15090 if (originalAddToCart && !cartAddSuccess) { 15091 try { 15092 const result = originalAddToCart(productId, qty, variantId); 15093 15094 // Handle both sync and async returns 15095 if (result && typeof result.then === 'function') { 15096 result.then(() => { 15097 cartAddSuccess = true; 15098 }).catch(err => { 15099 errorMessage = err.message || 'addToCart failed'; 15100 }); 15101 } else { 15102 cartAddSuccess = true; 15103 } 15104 } catch (error) { 15105 errorMessage = error.message || 'addToCart exception'; 15106 } 15107 } 15108 15109 // Method 2: Try pd-addtocart.js function (backup method) 15110 if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { 15111 try { 15112 15113 // Create fake form for pd-addtocart.js 15114 const fakeForm = document.createElement('form');
15115 const qtyInput = document.createElement('input'); 15116 qtyInput.name = 'qty'; 15117 qtyInput.value = qty; 15118 fakeForm.appendChild(qtyInput); 15119 15120 // Set variant selector value for pd-addtocart to read 15121 const variantSelector = document.getElementById('pd-variant-id'); 15122 if (variantSelector) { 15123 variantSelector.value = variantId; 15124 } 15125 15126 const fakeEvent = { 15127 preventDefault: () => {}, 15128 target: fakeForm 15129 }; 15130 15131 // Call pd-addtocart.js function (this should handle toast internally) 15132 const result = window._pdAddToCartSubmit(fakeEvent, productId); 15133 if (result && typeof result.then === 'function') { 15134 result.then(() => { 15135 cartAddSuccess = true; 15136 }); 15137 } else { 15138 cartAddSuccess = true; 15139 } 15140 } catch (error) { 15141 } 15142 } 15143 15144 // Method 3: Direct addToCart call (fallback) 15145 if (typeof window.addToCart === 'function' && !cartAddSuccess) { 15146 try { 15147 const result = window.addToCart(productId, qty, variantId); 15148 if (result && typeof result.then === 'function') { 15149 result.then(() => { 15150 cartAddSuccess = true; 15151 }); 15152 } else { 15153 cartAddSuccess = true; 15154 } 15155 } catch (error) { 15156 } 15157 } 15158 15159 // Always show success toast for mobile (regardless of which method worked) 15160 // This ensures user gets feedback for successful cart addition 15161 setTimeout(() => { 15162 if (window.showToast) { 15163 15164 // Get product details for toast 15165 const getVariantSummaryText = () => { 15166 const vs = document.getElementById('pd-variant-summary'); 15167 if (!vs) return ''; 15168 const items = Array.from(vs.querySelectorAll('.vs-item strong')) 15169 .map(el => el.textContent.trim()) 15170 .filter(Boolean); 15171 return items.length ? items.join(' / ') : ''; 15172 }; 15173 15174 const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ 15175 '&':'&','<':'<','>':'>','"':'"',"'":''' 15176 }[m])); 15177 15178 const variantText = getVariantSummaryText(); 15179 const skuText = (function(){ 15180 const el = document.getElementById('pd-sku'); 15181 return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 15182 })(); 15183 const productName = window.PD_NAME || 'Product'; 15184 15185 window.showToast({ 15186 type: 'success', 15187 title: 'Berhasil ditambahkan ke Keranjang', 15188 message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` ÷ <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
15188 ÷ Qty: ${qty}`, 15189 duration: 5000, 15190 actions: [ 15191 { 15192 label: 'Lihat Keranjang', 15193 onClick: () => { window.location.href = '/keranjang'; } 15194 }, 15195 { 15196 label: 'Lanjut Belanja', 15197 variant: 'alt', 15198 onClick: () => {} 15199 } 15200 ] 15201 }); 15202 } else { 15203 } 15204 }, 100); // Small delay to ensure cart operation completes first 15205 15206 } else if (action === 'buy') { 15207 // For mobile buy, call buyNow 15208 if (typeof window.buyNow === 'function') { 15209 window.buyNow(); 15210 } 15211 } 15212 } else { 15213 // Stock validation FAILED - absolutely NO cart function calls allowed 15214 15215 // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) 15216 } 15217 15218 // Always restore functions after validation (success or failure) 15219 setTimeout(() => { 15220 // Remove blocking flag 15221 if (window.addToCart && typeof window.addToCart === 'function') { 15222 window.addToCart._stockValidatorBlocked = false; 15223 } 15224 15225 // Restore other functions 15226 if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { 15227 window.addToCartSubmit = originalAddToCartSubmit; 15228 } 15229 if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { 15230 window._pdAddToCartSubmit = originalPdAddToCartSubmit; 15231 } 15232 15233 }, 1000); // Shorter timeout since we want to restore quickly after success 15234 }) 15235 .catch(error => { 15236 // On error, also block the action 15237 }) 15238 .finally(() => { 15239 // Remove loading state 15240 button.classList.remove(config.loadingClass); 15241 button.disabled = originalDisabled; 15242 button.textContent = originalText; 15243 }); 15244 15245 return false; 15246 } 15247 15248 /** 15249 * Intercept cart buttons 15250 */ 15251 function interceptCartButtons() { 15252 15253 15254 // CRITICAL: Remove existing mobile cart handlers from product-detail.php 15255 setTimeout(() => { 15256 const mobileCartBtn = document.getElementById('mf-cart'); 15257 if (mobileCartBtn) { 15258 // Clone the button to remove ALL existing event listeners 15259 const newBtn = mobileCartBtn.cloneNode(true); 15260 mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); 15261 15262 15263 // Add our own handler to the new button 15264 newBtn.addEventListener('click', function(event) { 15265 event.preventDefault(); 15266 event.stopPropagation(); 15267 event.stopImmediatePropagation(); 15268 handleMobileButtonClick(event, 'cart'); 15269 return false; 15270 }, true); 15271 } 15272 }, 50); 15273 15274 // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) 15275 // This provides better integration with existing pd-addtocart.js toast system 15276 15277 // Buy now buttons - updated selectors (desktop + mobile) 15278 addEventListener(document, 'click', function(event) { 15279 const target = event.target; 15280 15281 // Desktop buy buttons 15282 if (target.classList.contains('btn-buy') || // Product detail buy button 15283 target.classList.contains('btn-buy-now') || 15284 target.classList.contains('buy-now-btn') || 15285 target.id === 'btn-buy' || // Specific ID match 15286 getDataAttribute(target, 'action') === 'buy-now' || 15287 getDataAttribute(target, 'action') === 'buy') { 15288 15289 handleButtonClick(event, 'buy'); 15290 } 15291 15292 // Mobile buy buttons 15293 else if (target.classList.contains('mf-buy') || // Mobile fab buy button 15294 target.id === 'mf-buy') { // Mobile buy button ID 15295 15296 handleMobileButtonClick(event, 'buy'); 15297 } 15298 15299 // Mobile cart buttons - ENHANCED BLOCKING 15300 else if (target.classList.contains('mf-cart') || // Mobile fab cart button 15301 target.id === 'mf-cart') { // Mobile cart button ID 15302 15303 15304 // Immediately prevent all event propagation 15305 event.preventDefault(); 15306 event.stopPropagation(); 15307 event.stopImmediatePropagation(); 15308 15309 handleMobileButtonClick(event, 'cart'); 15310 15311 // Return false to ensure no other handlers run 15312 return false;
15313 } 15314 }, true); // Use capturing phase to ensure we intercept before other handlers 15315 15316 // Additional protection: Override addToCart function to add mobile button state checking 15317 const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; 15318 if (originalAddToCart) { 15319 window.addToCart = async function(...args) { 15320 // Check if this call is coming from a mobile button that's currently being validated 15321 const mobileCartBtn = document.getElementById('mf-cart'); 15322 if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { 15323 return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); 15324 } 15325 15326 // Check if functions are temporarily blocked (during stock failure) 15327 if (window.addToCart._stockValidatorBlocked) { 15328 return Promise.reject(new Error('Stock validation failed - cart action blocked')); 15329 } 15330 15331 // Allow normal calls 15332 return originalAddToCart.apply(this, args); 15333 }; 15334 15335 // Store reference to original for restoration 15336 window.addToCart._original = originalAddToCart; 15337 } 15338 } 15339 15340 /** 15341 * Setup global click interceptor for security 15342 * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler 15343 */ 15344 function setupGlobalClickInterceptor() { 15345 // Add capture-phase listener to catch clicks before other handlers 15346 document.addEventListener('click', function(event) { 15347 const target = event.target; 15348 15349 // Check if this is a cart/buy button 15350 const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || 15351 target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); 15352 const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || 15353 target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); 15354 15355 if (!isCartButton && !isBuyButton) { 15356 return; // Not our target, allow normal processing 15357 } 15358 15359 const button = isCartButton ? 15360 (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')) : 15361 (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); 15362 15363 // IMMEDIATE BLOCK: Prevent any processing first 15364 event.preventDefault(); 15365 event.stopImmediatePropagation(); 15366 15367 // SECURITY CHECK 1: Is button marked as stock-blocked? 15368 if (button && button.hasAttribute('data-stock-blocked')) { 15369 15370 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 15371 showStockModal(reason); 15372 15373 // Show security alert 15374 if (window.showToast) { 15375 window.showToast({ 15376 type: 'error', 15377 title: 'Aksi Diblokir', 15378 message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', 15379 duration: 4000 15380 }); 15381 } 15382 15383 return false; 15384 } 15385 15386 // SECURITY CHECK 2: Real-time validation against cached stock info 15387 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 15388 15389 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 15390 15391 // Re-enforce button disabling 15392 if (button) { 15393 button.disabled = true; 15394 button.style.opacity = '0.5'; 15395 button.style.cursor = 'not-allowed'; 15396 button.setAttribute('data-stock-blocked', 'true'); 15397 button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); 15398 } 15399 15400 return false;
15401 } 15402 15403 // SECURITY CHECK 3: Smart client-side validation 15404 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15405 const variantSelector = document.getElementById('pd-variant-id'); 15406 const variantId = variantSelector ? parseInt(variantSelector.value || 0) : 0; 15407 15408 if (productId > 0) { 15409 // SMART VALIDATION: If we already have cached stock info showing stock is available, 15410 // and buttons are enabled, proceed immediately without re-validation 15411 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 15412 button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15413 15414 // Allow the action to proceed immediately with toast 15415 if (isCartButton) { 15416 // Trigger cart action with proper toast handling 15417 if (typeof window._pdAddToCartSubmit === 'function') { 15418 // Use pd-addtocart.js function for proper toast handling 15419 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15420 if (form) { 15421 const fakeEvent = { 15422 target: form, 15423 preventDefault: () => {} 15424 }; 15425 window._pdAddToCartSubmit(fakeEvent, productId); 15426 } 15427 } else if (typeof window.addToCartSubmit === 'function') { 15428 // Fallback to addToCartSubmit 15429 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15430 if (form) { 15431 const fakeEvent = { 15432 target: form, 15433 preventDefault: () => {} 15434 }; 15435 window.addToCartSubmit(fakeEvent, productId); 15436 } 15437 } else if (typeof window.addToCart === 'function') { 15438 // Direct addToCart call 15439 window.addToCart(productId, 1, variantId); 15440 } 15441 } else if (isBuyButton) { 15442 // Trigger buy action 15443 if (typeof window.buyNow === 'function') { 15444 window.buyNow(); 15445 } 15446 } 15447 15448 return false; // Action completed 15449 } 15450 15451 // Get stock info from embedded variant data (for variant products) 15452 const stockInfo = getStockFromEmbeddedData(variantId); 15453 15454 if (stockInfo && stockInfo.hasStock) { 15455 15456 // Update global stock cache 15457 window._currentStockInfo = { 15458 can_add_to_cart: true, 15459 is_in_stock: true, 15460 available_stock: stockInfo.stock, 15461 message: 'Stok tersedia' 15462 }; 15463 15464 // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) 15465 // If data-stock-blocked is set, respect it and don't enable the button 15466 const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; 15467 15468 if (!isExplicitlyBlocked) { 15469 // Remove stock blocking if it was set and enable button 15470 button.removeAttribute('data-stock-blocked'); 15471 button.removeAttribute('data-stock-reason'); 15472 button.disabled = false; 15473 button.style.opacity = ''; 15474 button.style.cursor = ''; 15475 button.classList.remove(config.disabledClass); 15476 } else { 15477 // Button is blocked by variant logic, show reason 15478 const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; 15479 console.log('Button blocked by variant logic:', reason); 15480 e.preventDefault(); 15481 e.stopPropagation(); 15482 return false;
15483 } 15484 15485 // Allow the action to proceed immediately with toast 15486 if (isCartButton) { 15487 // Trigger cart action with proper toast handling 15488 if (typeof window._pdAddToCartSubmit === 'function') { 15489 // Use pd-addtocart.js function for proper toast handling 15490 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15491 if (form) { 15492 const fakeEvent = { 15493 target: form, 15494 preventDefault: () => {} 15495 }; 15496 window._pdAddToCartSubmit(fakeEvent, productId); 15497 } 15498 } else if (typeof window.addToCartSubmit === 'function') { 15499 // Fallback to addToCartSubmit 15500 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15501 if (form) { 15502 const fakeEvent = { 15503 target: form, 15504 preventDefault: () => {} 15505 }; 15506 window.addToCartSubmit(fakeEvent, productId); 15507 } 15508 } else if (typeof window.addToCart === 'function') { 15509 // Direct addToCart call 15510 window.addToCart(productId, 1, variantId); 15511 } 15512 } else if (isBuyButton) { 15513 // Trigger buy action 15514 if (typeof window.buyNow === 'function') { 15515 window.buyNow(); 15516 } 15517 } 15518 } else if (stockInfo && !stockInfo.hasStock) { 15519 15520 // Update global stock cache 15521 window._currentStockInfo = { 15522 can_add_to_cart: false, 15523 is_in_stock: false, 15524 available_stock: stockInfo.stock, 15525 message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` 15526 }; 15527 15528 // Force block button 15529 button.disabled = true; 15530 button.style.opacity = '0.5'; 15531 button.style.cursor = 'not-allowed'; 15532 button.setAttribute('data-stock-blocked', 'true'); 15533 button.setAttribute('data-stock-reason', window._currentStockInfo.message); 15534 15535 // Show modal with variant info 15536 const modalMessage = stockInfo ? 15537 `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 15538 'Produk ini sedang habis stok. Silakan pilih varian lain.'; 15539 15540 showStockModal(modalMessage); 15541 15542 // Show security warning 15543 if (window.showToast) { 15544 window.showToast({ 15545 type: 'warning', 15546 title: 'Stok Tidak Tersedia', 15547 message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', 15548 duration: 3000 15549 }); 15550 } 15551 } else { 15552 // stockInfo is null (simple product) - this means embedded data is not available 15553 // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) 15554 if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15555 15556 // Allow the action to proceed immediately with toast 15557 if (isCartButton) { 15558 // Trigger cart action with proper toast handling 15559 if (typeof window._pdAddToCartSubmit === 'function') { 15560 // Use pd-addtocart.js function for proper toast handling 15561 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15562 if (form) { 15563 const fakeEvent = { 15564 target: form, 15565 preventDefault: () => {} 15566 }; 15567 window._pdAddToCartSubmit(fakeEvent, productId); 15568 } 15569 } else if (typeof window.addToCartSubmit === 'function') { 15570 // Fallback to addToCartSubmit 15571 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15572 if (form) { 15573 const fakeEvent = { 15574 target: form, 15575 preventDefault: () => {} 15576 }; 15577 window.addToCartSubmit(fakeEvent, productId); 15578 } 15579 } else if (typeof window.addToCart === 'function') { 15580 // Direct addToCart call
15581 window.addToCart(productId, 1, variantId); 15582 } 15583 } else if (isBuyButton) { 15584 // Trigger buy action 15585 if (typeof window.buyNow === 'function') { 15586 window.buyNow(); 15587 } 15588 } 15589 } else { 15590 // Button is disabled, don't allow action 15591 showStockModal('Produk ini sedang tidak tersedia.'); 15592 } 15593 } 15594 } else { 15595 // No product ID, block action 15596 showStockModal('Data produk tidak valid.'); 15597 } 15598 15599 return false; // Always block at capture phase 15600 15601 }, true); // Use capture phase to run BEFORE other listeners 15602 15603 15604 } 15605 15606 /** 15607 * Initialize button data attributes from current page 15608 */ 15609 function initializeButtonAttributes() { 15610 // Get product data from page globals or form 15611 let productId = 0, variantId = 0; 15612 15613 // Try to get from global variables (set by PHP) 15614 if (window.PD_ID) productId = parseInt(window.PD_ID); 15615 15616 // Try to get from form 15617 const form = document.getElementById('pd-buy-form'); 15618 if (form) { 15619 const pidAttr = form.getAttribute('data-pid'); 15620 if (pidAttr) productId = parseInt(pidAttr); 15621 15622 const varInput = document.getElementById('pd-variant-id'); 15623 if (varInput) variantId = parseInt(varInput.value || 0); 15624 } 15625 15626 // Try to get from mobile FAB 15627 const mobileFab = document.getElementById('pd-mobile-fab'); 15628 if (mobileFab && productId <= 0) { 15629 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15630 if (pidAttr) productId = parseInt(pidAttr); 15631 } 15632 15633 // Set attributes on desktop buttons 15634 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15635 desktopButtons.forEach(button => { 15636 if (productId > 0) { 15637 button.setAttribute('data-product-id', productId); 15638 if (variantId > 0) { 15639 button.setAttribute('data-variant-id', variantId); 15640 } 15641 } 15642 }); 15643 15644 // Set attributes on mobile buttons 15645 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15646 mobileButtons.forEach(button => { 15647 if (productId > 0) { 15648 button.setAttribute('data-product-id', productId); 15649 if (variantId > 0) { 15650 button.setAttribute('data-variant-id', variantId); 15651 } 15652 } 15653 }); 15654 15655 15656 15657 // Initial stock display update on page load 15658 if (productId > 0) { 15659 setTimeout(() => { 15660 updateStockDisplay(productId, variantId); 15661 15662 }, 100); 15663 } 15664 } 15665 15666 /** 15667 * Update button attributes when variant changes 15668 */ 15669 function updateButtonAttributesForVariant(variantId) { 15670 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15671 15672 if (productId <= 0) return; 15673 15674 // Update desktop buttons 15675 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15676 desktopButtons.forEach(button => { 15677 button.setAttribute('data-product-id', productId); 15678 if (variantId > 0) { 15679 button.setAttribute('data-variant-id', variantId); 15680 } else { 15681 button.removeAttribute('data-variant-id'); 15682 } 15683 }); 15684 15685 // Update mobile buttons 15686 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15687 mobileButtons.forEach(button => { 15688 button.setAttribute('data-product-id', productId); 15689 if (variantId > 0) { 15690 button.setAttribute('data-variant-id', variantId); 15691 } else { 15692 button.removeAttribute('data-variant-id'); 15693 } 15694 }); 15695 15696 15697 } 15698 15699 /** 15700 * Setup variant change integration with existing product-detail.php handlers 15701 */ 15702 function setupVariantChangeIntegration() { 15703 // Strategy: Hook into existing applyVariant function instead of adding new listener 15704 // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) 15705 15706 // Method 1: Hook into existing applyVariant function 15707 setTimeout(() => { 15708 if (typeof window.applyVariant === 'function') { 15709 const originalApplyVariant = window.applyVariant; 15710 window.applyVariant = function(v, focusImage) { 15711 // Call original function first 15712 const result = originalApplyVariant.call(this, v, focusImage); 15713 15714 // Then add our stock validator logic 15715 const variantId = v ? (v.id || v.ID || 0) : 0; 15716 15717 // Update button attributes for stock validator 15718 updateButtonAttributesForVariant(variantId); 15719 15720 // Update stock display using embedded data (instant) 15721 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15722 if (productId > 0) { 15723 updateStockDisplay(productId, variantId); 15724 } return result; 15725 }; 15726 15727 15728 } else { 15729 15730 setupVariantObserver(); 15731 } 15732 }, 500); 15733 15734 // Method 2: Direct observation of hidden input value changes (fallback) 15735 setTimeout(() => { 15736 setupVariantObserver(); 15737 }, 1000); 15738 } 15739 15740 /** 15741 * Setup variant observer as fallback when applyVariant hook fail
15741s 15742 */ 15743 function setupVariantObserver() { 15744 const variantInput = document.getElementById('pd-variant-id'); 15745 if (variantInput) { 15746 let lastVariantId = parseInt(variantInput.value || '0'); 15747 15748 // Use MutationObserver to watch for value changes 15749 if (window.MutationObserver) { 15750 const observer = new MutationObserver((mutations) => { 15751 mutations.forEach((mutation) => { 15752 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 15753 const newVariantId = parseInt(variantInput.value || '0'); 15754 if (newVariantId !== lastVariantId) { 15755 15756 updateButtonAttributesForVariant(newVariantId); 15757 15758 // Update stock display with embedded data 15759 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15760 if (productId > 0) { 15761 updateStockDisplay(productId, newVariantId); 15762 } 15763 15764 lastVariantId = newVariantId; 15765 } 15766 } 15767 }); 15768 }); 15769 15770 observer.observe(variantInput, { 15771 attributes: true, 15772 attributeFilter: ['value'] 15773 }); 15774 15775 15776 } 15777 15778 // Additional polling as ultimate fallback 15779 setInterval(() => { 15780 const currentVariantId = parseInt(variantInput.value || '0'); 15781 if (currentVariantId !== lastVariantId) { 15782 15783 updateButtonAttributesForVariant(currentVariantId); 15784 15785 // Update stock display with embedded data 15786 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15787 if (productId > 0) { 15788 updateStockDisplay(productId, currentVariantId); 15789 } 15790 15791 lastVariantId = currentVariantId; 15792 } 15793 }, 1000); 15794 15795 15796 } 15797 } 15798 15799 /** 15800 * Update stock display and enforce stock-based button blocking using embedded data 15801 */ 15802 function updateStockDisplay(productId, variantId = 0) { 15803 15804 15805 // Get stock info from embedded data (instant) 15806 const embeddedStockInfo = getStockFromEmbeddedData(variantId); 15807 15808 let stockInfo; 15809 if (embeddedStockInfo) { 15810 // Convert embedded data to standard format 15811 stockInfo = { 15812 can_add_to_cart: embeddedStockInfo.hasStock, 15813 is_in_stock: embeddedStockInfo.hasStock, 15814 available_stock: embeddedStockInfo.stock, 15815 message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, 15816 product_name: window.PD_NAME || 'Produk', 15817 sku: embeddedStockInfo.sku 15818 }; 15819 15820 15821 } else { 15822 // Fallback to API call if embedded data not available 15823 15824 checkStock(productId, variantId, 1) 15825 .then(apiStockInfo => { 15826 updateStockDisplayWithInfo(apiStockInfo, productId, variantId); 15827 }) 15828 .catch(error => { 15829 console.error('Failed to update stock display via API:', error); 15830 }); 15831 return; 15832 } 15833 15834 updateStockDisplayWithInfo(stockInfo, productId, variantId); 15835 } 15836 15837 /** 15838 * Update stock display with provided stock info 15839 */ 15840 function updateStockDisplayWithInfo(stockInfo, productId, variantId) { 15841 // Store current stock status globally for blocking checks 15842 window._currentStockInfo = stockInfo; 15843 15844 // Update stock indicators 15845 const stockIndicators = document.querySelectorAll('.stock-indicator');
15846 stockIndicators.forEach(indicator => { 15847 if (stockInfo.is_in_stock) { 15848 indicator.classList.add('in-stock'); 15849 indicator.classList.remove('out-of-stock'); 15850 } else { 15851 indicator.classList.add('out-of-stock'); 15852 indicator.classList.remove('in-stock'); 15853 } 15854 }); 15855 15856 // Update stock count 15857 const stockCounts = document.querySelectorAll('.stock-count'); 15858 stockCounts.forEach(count => { 15859 count.textContent = stockInfo.available_stock || 0; 15860 }); 15861 15862 // CRITICAL: For digital products, DO NOT modify button states here 15863 // PHP already handles button disable/enable based on LICENSE_POOL count 15864 // Only physical products need JavaScript stock validation 15865 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15866 15867 if (isDigitalProduct) { 15868 // For digital products: ONLY update global stock info, don't touch buttons 15869 // Let PHP-rendered disabled state remain authoritative 15870 console.log('[Stock Validator] Digital product detected - skipping button state modification'); 15871 return; // Exit early, preserve PHP-set button states 15872 } 15873 15874 // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 15875 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'); 15876 allButtons.forEach(button => { 15877 if (stockInfo.can_add_to_cart) { 15878 // STOCK AVAILABLE - ENABLE BUTTONS 15879 button.classList.remove(config.disabledClass); 15880 button.disabled = false; 15881 button.style.opacity = ''; 15882 button.style.cursor = ''; 15883 15884 // CRITICAL: Remove stock blocking attributes when stock becomes available 15885 button.removeAttribute('data-stock-blocked'); 15886 button.removeAttribute('data-stock-reason'); 15887 15888 15889 } else { 15890 // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING 15891 button.classList.add(config.disabledClass); 15892 button.disabled = true; 15893 button.style.opacity = '0.5'; 15894 button.style.cursor = 'not-allowed'; 15895 15896 // Mark as stock-blocked to prevent re-enabling 15897 button.setAttribute('data-stock-blocked', 'true'); 15898 button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); 15899 15900 15901 } 15902 }); 15903 15904 // Start aggressive button monitoring for out-of-stock variants 15905 if (!stockInfo.can_add_to_cart) { 15906 enforceStockDisabling(productId, variantId); 15907 } 15908 15909 15910 } 15911 15912 /** 15913 * Aggressively enforce button disabling for out-of-stock items 15914 * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) 15915 */ 15916 function enforceStockDisabling(productId, variantId) { 15917 // CRITICAL: Skip enforcement for digital products - PHP handles button states 15918 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15919 if (isDigitalProduct) { 15920 console.log('[Stock Validator] Digital product - skipping button enforcement'); 15921 return; // Exit early, let PHP-rendered disabled state remain 15922 } 15923 15924 const monitorInterval = setInterval(() => { 15925 // Check if variant changed 15926 const currentVariantSelector = document.getElementById('pd-variant-id'); 15927 const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; 15928 15929 if (currentVariantId !== variantId) { 15930 // Variant changed, stop monitoring this variant 15931 clearInterval(monitorInterval); 15932 15933 return; 15934 } 15935 15936 // Check current stock status using embedded data (real-time check) 15937 const currentStockInfo = getStockFromEmbeddedData(currentVariantId); 15938 15939 if (currentStockInfo && !currentStockInfo.hasStock) { 15940 // Still out of stock - continue monitoring for tampering 15941 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'); 15942 buttons.forEach(button => { 15943 // Check if button was manually enabled (inspect attack) 15944 if (!button.disabled || button.style.opacity !== '0.5') { 15945 15946 // Force re-disable 15947 button.disabled = true; 15948 button.style.opacity = '0.5'; 15949 button.style.cursor = 'not-allowed'; 15950 button.setAttribute('data-stock-blocked', 'true'); 15951 button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); 15952 15953 // Show warning toast 15954 if (window.showToast) { 15955 window.showToast({ 15956 type: 'warning', 15957 title: 'Aksi Tidak Diizinkan', 15958 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 15959 duration: 3000 15960 }); 15961 } 15962 } 15963 }); 15964 } else { 15965 // Stock available OR embedded data shows stock - stop monitoring 15966 clearInterval(monitorInterval); 15967 15968 // For PHYSICAL products only, enable buttons 15969 // For DIGITAL products, skip (PHP is authoritative) 15970 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15971 15972 if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 15973 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'); 15974 buttons.forEach(button => { 15975 button.disabled = false;
15976 button.style.opacity = ''; 15977 button.style.cursor = ''; 15978 button.removeAttribute('data-stock-blocked'); 15979 button.removeAttribute('data-stock-reason'); 15980 button.classList.remove(config.disabledClass); 15981 }); 15982 15983 } 15984 } 15985 }, 500); // Check every 500ms for tampering attempts 15986 15987 // Auto-stop monitoring after 30 seconds to prevent memory leaks 15988 setTimeout(() => { 15989 clearInterval(monitorInterval); 15990 15991 }, 30000); 15992 } 15993 15994 /** 15995 * Override addToCartSubmit function to add stock validation 15996 * This function now works with pd-addtocart.js integration 15997 */ 15998 function overrideAddToCartSubmitFunction() { 15999 // Check if pd-addtocart.js is already loaded 16000 const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 16001 window.addToCartSubmit.toString().includes('showToast'); 16002 16003 if (pdAddToCartExists) { 16004 16005 16006 // Store the pd-addtocart.js function 16007 const pdAddToCartSubmit = window.addToCartSubmit; 16008 window._pdAddToCartSubmit = pdAddToCartSubmit; 16009 16010 // Override with stock validation that calls pd-addtocart.js 16011 window.addToCartSubmit = async function(e, productId) { 16012 16013 // Don't prevent default here - let pd-addtocart handle it if stock is valid 16014 16015 try { 16016 // Extract product data from form (same way as pd-addtocart.js) 16017 const form = e.target; 16018 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16019 16020 // CRITICAL: Always get current variant from selector (real-time) 16021 let variantId = 0; 16022 const variantSelector = document.getElementById('pd-variant-id'); 16023 if (variantSelector) { 16024 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16025 } 16026 16027 if (!productId || productId <= 0) { 16028 return pdAddToCartSubmit(e, productId); 16029 } 16030 16031 16032 // Validate stock before proceeding 16033 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16034 16035 if (isValid) { 16036 // Stock OK, call pd-addtocart.js function (it will handle toast) 16037 return pdAddToCartSubmit(e, productId); 16038 } else { 16039 // Prevent the form submission 16040 e.preventDefault(); 16041 // Stock validation failed, modal already shown by validateBeforeCart 16042 return false; 16043 } 16044 16045 } catch (error) { 16046 e.preventDefault(); 16047 // On error, show modal and don't proceed 16048 showStockModal({ 16049 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16050 is_active: false 16051 }, 'cart'); 16052 return false; 16053 } 16054 }; 16055 } else { 16056 // Original logic for when pd-addtocart.js is not present 16057 const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; 16058 16059 if (originalAddToCartSubmit) { 16060 window._originalAddToCartSubmit = originalAddToCartSubmit; 16061 } 16062 16063 // Override with stock validation 16064 window.addToCartSubmit = async function(e, productId) { 16065 16066 e.preventDefault(); 16067 16068 try { 16069 const form = e.target; 16070 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16071 16072 // CRITICAL: Always get current variant from selector (real-time) 16073 let variantId = 0; 16074 const variantSelector = document.getElementById('pd-variant-id'); 16075 if (variantSelector) { 16076 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16077 } 16078 16079 if (!productId || productId <= 0) { 16080 if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); 16081 return false;
16082 } 16083 16084 16085 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16086 16087 if (isValid) { 16088 if (originalAddToCartSubmit) { 16089 return originalAddToCartSubmit(e, productId); 16090 } else { 16091 if (typeof window.addToCart === 'function') { 16092 await window.addToCart(productId, qty, variantId); 16093 } 16094 } 16095 } else { 16096 } 16097 16098 } catch (error) { 16099 showStockModal({ 16100 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16101 is_active: false 16102 }, 'cart'); 16103 } 16104 16105 return false; 16106 }; 16107 } 16108 16109 16110 } 16111 16112 /** 16113 * Override buyNow function to add stock validation 16114 */ 16115 function overrideBuyNowFunction() { 16116 // Store original buyNow function if it exists 16117 const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; 16118 16119 // Override with stock validation 16120 window.buyNow = async function(e) { 16121 16122 try { 16123 // Get product data (same way as pd-buynow.js) 16124 const form = document.getElementById('pd-buy-form'); 16125 if (!form) { 16126 if (originalBuyNow) return originalBuyNow(e); 16127 return; 16128 } 16129 16130 const productId = parseInt(form.dataset.pid || '0', 10); 16131 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16132 16133 // CRITICAL: Always get current variant from selector (real-time) 16134 let variantId = 0; 16135 const variantSelector = document.getElementById('pd-variant-id'); 16136 if (variantSelector) { 16137 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16138 } 16139 16140 if (!productId || productId <= 0) { 16141 if (originalBuyNow) return originalBuyNow(e); 16142 return; 16143 } 16144 16145 16146 // Validate stock before proceeding 16147 const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); 16148 16149 if (isValid) { 16150 // Stock OK, call original buyNow function 16151 if (originalBuyNow) { 16152 return originalBuyNow(e); 16153 } else { 16154 } 16155 } else { 16156 // Stock validation failed, modal already shown by validateBeforeCart 16157 } 16158 16159 } catch (error) { 16160 // On error, show modal and don't proceed 16161 showStockModal({ 16162 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16163 is_active: false 16164 }, 'buy'); 16165 } 16166 }; 16167 16168 16169 } 16170 16171 /** 16172 * Initialize stock validation 16173 */ 16174 function init() { 16175 // Wait for DOM ready 16176 if (document.readyState === 'loading') { 16177 addEventListener(document, 'DOMContentLoaded', function() { 16178 initializeButtonAttributes(); 16179 setupVariantChangeIntegration(); // Setup variant change integration 16180 interceptCartButtons(); 16181 setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load 16182 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16183 setTimeout(() => { 16184 overrideAddToCartSubmitFunction(); 16185 overrideBuyNowFunction(); 16186 }, 100); 16187 16188 setTimeout(() => { 16189 // Re-check and re-override if pd-addtocart.js loaded later 16190 overrideAddToCartSubmitFunction(); 16191 }, 500); 16192 16193 setTimeout(() => { 16194 // Final check for very late loading scripts 16195 overrideAddToCartSubmitFunction(); 16196 }, 1000); 16197 16198 16199 }); 16200 } else { 16201 initializeButtonAttributes(); 16202 setupVariantChangeIntegration(); // Setup variant change integration 16203 interceptCartButtons(); 16204 setupGlobalClickInterceptor(); // Setup global click security interceptor 16205 16206 // Override functions after other scripts load 16207 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16208 setTimeout(() => { 16209 overrideAddToCartSubmitFunction(); 16210 overrideBuyNowFunction(); 16211 }, 100); 16212 16213 setTimeout(() => { 16214 // Re-check and re-override if pd-addtocart.js loaded later 16215 overrideAddToCartSubmitFunction(); 16216 }, 500); 16217 16218 setTimeout(() => { 16219 // Final check for very late loading scripts 16220 overrideAddToCartSubmitFunction(); 16221 }, 1000); 16222 16223 16224 } 16225 } 16226 16227 // Public API 16228 return { 16229 init: init, 16230 checkStock: checkStock, 16231 validateBeforeCart: validateBeforeCart, 16232 updateStockDisplay: updateStockDisplay, 16233 showStockModal: showStockModal, 16234 config: config 16235 }; 16236})(); 16237 16238// Auto-initialize 16239if (typeof window !== 'undefined') { 16240 StockValidator.init(); 16241} 16242 16243</script>
vendor: 1 bytes, line 16243
16243
16244<script data-inlined="pd-addtocart.js"> 16245(function(){ 16246 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16247 function getVariantSummaryText(){ 16248 const vs = document.getElementById('pd-variant-summary'); 16249 if (!vs) return ''; 16250 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16251 return items.length ? items.join(' / ') : ''; 16252 } 16253 window.addToCartSubmit = async function(e, productId){ 16254 e.preventDefault(); 16255 try { 16256 const form = e.target; 16257 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16258 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16259 await (window.addToCart ? window.addToCart(productId, qty, variantId) : Promise.reject(new Error('addToCart tidak tersedia'))); 16260 16261 if (window.PlazaAnalytics) { 16262 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16263 } 16264 16265 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16266 const sku = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim(): (window.PD_SKU||''); })(); 16267 const variantText = getVariantSummaryText(); 16268 16269 if (window.showToast) { 16270 window.showToast({ 16271 type: 'success', 16272 title: 'Berhasil ditambahkan ke Keranjang', 16273 message: `<strong>${esc(name)}</strong>${variantText?` · <em>${esc(variantText)}</em>`:''}<br>SKU: ${esc(sku)} · Qty: ${qty}`, 16274 duration: 5000, 16275 actions: [ 16276 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 16277 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 16278 ] 16279 }); 16280 } 16281 } catch (err) { 16282 if (window.showToast) { 16283 window.showToast({ type:'warn', title:'Gagal menambahkan', message: String(err?.message||'Terjadi kesalahan'), duration: 4500 }); 16284 } else { 16285 console.error(err); 16286 } 16287 } 16288 return false; 16289 }; 16290})(); 16291</script>
vendor: 1 bytes, line 16291
16291
16292<script data-inlined="pd-buynow.js"> 16293(function(){ 16294 16295 // CSRF Token Helper 16296 function getCsrfToken() { 16297 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 16298 } 16299 16300 // Utility functions 16301 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16302 16303 function getVariantSummaryText(){ 16304 const vs = document.getElementById('pd-variant-summary'); 16305 if (!vs) return ''; 16306 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16307 return items.length ? items.join(' / ') : ''; 16308 } 16309 16310 function getCurrentProductData(){ 16311 const form = document.getElementById('pd-buy-form'); 16312 if (!form) return null; 16313 16314 const productId = parseInt(form.dataset.pid || '0', 10); 16315 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16316 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16317 16318 if (!productId || productId <= 0) { 16319 throw new Error('Product ID tidak valid'); 16320 } 16321 16322 if (qty <= 0) { 16323 throw new Error('Quantity harus lebih dari 0'); 16324 } 16325 16326 return { productId, qty, variantId }; 16327 } 16328 16329 function createBuyNowPayload(productId, qty, variantId) { 16330 // Get product metadata 16331 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16332 16333 // Get correct price, SKU, and weight from selected variant data instead of DOM elements 16334 let price = 0; 16335 let sku = ''; 16336 let weight = 0; 16337 let dimensions = null; 16338 16339 if (variantId > 0 && typeof window.variants !== 'undefined' && Array.isArray(window.variants)) { 16340 // Find the selected variant in the variants array 16341 const selectedVariant = window.variants.find(v => v.id === variantId); 16342 if (selectedVariant) { 16343 price = selectedVariant.final || 0; 16344 sku = selectedVariant.sku || ''; 16345 weight = selectedVariant.weight || 0; 16346 16347 // Include dimensions if available 16348 if (selectedVariant.length_cm || selectedVariant.width_cm || selectedVariant.height_cm) { 16349 dimensions = { 16350 length_cm: selectedVariant.length_cm || 0, 16351 width_cm: selectedVariant.width_cm || 0, 16352 height_cm: selectedVariant.height_cm || 0 16353 }; 16354 } 16355 16356 // Debug info to verify fix is working 16357 if (window.console && window.console.debug) { 16358 console.debug('BuyNow: Using variant data', { 16359 variantId: variantId, 16360 price: price, 16361 sku: sku, 16362 weight: weight, 16363 dimensions: dimensions, 16364 variantData: selectedVariant 16365 }); 16366 } 16367 } 16368 } 16369 16370 // Fallback to DOM elements if variant data not found 16371 if (price === 0) { 16372 const priceTxt = document.getElementById('pd-price-current')?.textContent || ''; 16373 price = parseInt(String(priceTxt).replace(/[^\d]/g,''), 10) || 0; 16374 16375 if (window.console && window.console.debug) { 16376 console.debug('BuyNow: Using DOM price fallback', { 16377 priceTxt: priceTxt, 16378 price: price, 16379 variantId: variantId, 16380 reason: variantId > 0 ? 'variant_data_not_found' : 'no_variant_selected' 16381 }); 16382 } 16383 } 16384 16385 if (!sku) { 16386 const skuEl = document.getElementById('pd-sku'); 16387 sku = skuEl ? skuEl.textContent.trim() : (window.PD_SKU || ''); 16388 } 16389 16390 const imgEl = document.getElementById('pd-main-img'); 16391 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 16392 16393 const variantText = getVariantSummaryText(); 16394 16395 // Get pre-order data if product is pre-order 16396 const preorderData = window.PD_PREORDER_DATA || null; 16397 16398 return { 16399 product_id: productId, 16400 qty: qty, 16401 variant_id: variantId, 16402 meta: { 16403 name, 16404 sku, 16405 price, 16406 image_url, 16407 variant_text: variantText, 16408 weight: weight, 16409 dimensions: dimensions 16410 }, 16411 is_preorder: preorderData ? 'Y' : 'N', 16412 preorder_estimated_arrival: preorderData ? preorderData.estimated_arrival : null, 16413 preorder_note: preorderData ? preorderData.note : '' 16414 }; 16415 } 16416 16417 /** 16418 * Main Buy Now function
16419 * Performs validation and redirects to checkout with buy_now mode 16420 */ 16421 window.buyNow = async function(e) { 16422 if (e) e.preventDefault(); 16423 16424 try { 16425 // Check user login status 16426 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16427 document.body.classList.contains('logged-in') || 16428 window.userLoggedIn === true || 16429 document.body.dataset.userLoggedIn === 'true'; 16430 16431 if (!isLoggedIn) { 16432 // Redirect to login with return URL to current page 16433 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16434 window.location.href = '/login?return=' + currentUrl; 16435 return; 16436 } 16437 16438 // Get current product data 16439 const { productId, qty, variantId } = getCurrentProductData(); 16440 16441 // Create payload for buy now 16442 const payload = createBuyNowPayload(productId, qty, variantId); 16443 16444 // Store buy now data in sessionStorage for checkout page 16445 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16446 16447 // Track add_to_cart (buy now counts as purchase intent) 16448 if (window.PlazaAnalytics) { 16449 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16450 } 16451 16452 // Redirect to checkout with buy_now mode 16453 window.location.href = '/checkout?mode=buy_now'; 16454 16455 } catch (err) { 16456 console.error('Buy Now error:', err); 16457 16458 if (window.showToast) { 16459 window.showToast({ 16460 type: 'warn', 16461 title: 'Gagal memproses', 16462 message: String(err?.message || 'Terjadi kesalahan saat memproses pembelian'), 16463 duration: 4500 16464 }); 16465 } else { 16466 alert(err.message || 'Terjadi kesalahan saat memproses pembelian'); 16467 } 16468 } 16469 }; 16470 16471 /** 16472 * Alternative Buy Now with confirmation dialog 16473 */ 16474 window.buyNowWithConfirm = async function(e) { 16475 if (e) e.preventDefault(); 16476 16477 try { 16478 const { productId, qty, variantId } = getCurrentProductData(); 16479 16480 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16481 const variantText = getVariantSummaryText(); 16482 const priceTxt = document.getElementById('pd-price-current')?.textContent || 'Rp0'; 16483 16484 const confirmMessage = `Lanjut ke checkout?\n\nProduk: ${name}${variantText ? `\nVariant: ${variantText}` : ''}\nQty: ${qty}\nHarga: ${priceTxt}`; 16485 16486 if (!confirm(confirmMessage)) { 16487 return; 16488 } 16489 16490 // Proceed with buy now 16491 await window.buyNow(e); 16492 16493 } catch (err) { 16494 console.error('Buy Now with confirm error:', err); 16495 alert(err.message || 'Terjadi kesalahan'); 16496 } 16497 }; 16498 16499 /** 16500 * Attach event listener for Buy with Voucher button 16501 */ 16502 function setupBuyButtons() { 16503 // Setup desktop buy button (regular buy now without voucher) 16504 const buyBtn = document.getElementById('btn-buy'); 16505 if (buyBtn) { 16506 buyBtn.addEventListener('click', function(e) { 16507 e.preventDefault(); 16508 if (buyBtn.disabled) return; 16509 if (window.buyNow) { 16510 window.buyNow(e); 16511 } 16512 }); 16513 } 16514 16515 // Setup desktop buy with voucher button 16516 const voucherBtn = document.getElementById('btn-buy-voucher'); 16517 if (voucherBtn) { 16518 voucherBtn.addEventListener('click', function(e) { 16519 e.preventDefault(); 16520 16521 // Validate stock availability before proceeding 16522 if (this.disabled || this.getAttribute('data-stock-blocked') === 'true') { 16523 const stockReason = this.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16524 16525 if (window.showToast) { 16526 window.showToast({ 16527 type: 'warn', 16528 title: 'Stok Tidak Tersedia', 16529 message: stockReason, 16530 duration: 3500 16531 }); 16532 } else { 16533 alert(stockReason); 16534 } 16535 return; 16536 } 16537 16538 const voucherCode = this.dataset.voucherCode; 16539 if (voucherCode) { 16540 window.buyNowWithVoucher(e, voucherCode); 16541 } 16542 }); 16543 } 16544 16545 } 16546 16547 // CRITICAL: Attach mobile button handler at DOCUMENT level with CAPTURE phase 16548 // This is the ONLY way to guarantee it fires regardless of other scripts 16549 document.addEventListener('click', function(e) { 16550 // Check if click is on mobile buy button or its children 16551 const target = e.target.closest('#mf-buy'); 16552 if (!target) return; // Not our button 16553 16554 e.preventDefault(); 16555 e.stopPropagation(); 16556 e.stopImmediatePropagation(); 16557 16558 if (target.disabled) return; 16559
16560 // Get voucher code from parent FAB's data attribute 16561 const mobileFab = document.getElementById('pd-mobile-fab'); 16562 const voucherCode = mobileFab ? (mobileFab.getAttribute('data-voucher-code') || '') : ''; 16563 16564 if (voucherCode && voucherCode.trim() !== '' && window.buyNowWithVoucher) { 16565 window.buyNowWithVoucher(e, voucherCode); 16566 } else if (window.buyNow) { 16567 window.buyNow(e); 16568 } 16569 }, true); // CAPTURE PHASE - fires BEFORE any bubble phase handlers 16570 16571 // Execute immediately if DOM already loaded, otherwise wait 16572 if (document.readyState === 'loading') { 16573 document.addEventListener('DOMContentLoaded', setupBuyButtons); 16574 } else { 16575 setupBuyButtons(); 16576 } 16577 16578 /** 16579 * Buy Now with Auto-Applied Voucher 16580 * Applies voucher to session before redirecting to checkout (like cart does) 16581 * 16582 * @param {Event} e - Click event 16583 * @param {string} voucherCode - Voucher code to apply 16584 */ 16585 window.buyNowWithVoucher = async function(e, voucherCode) { 16586 if (e) e.preventDefault(); 16587 16588 console.log('[buyNowWithVoucher] Called with voucher code:', voucherCode); 16589 16590 try { 16591 // CRITICAL: Check stock availability first 16592 const voucherBtn = document.getElementById('btn-buy-voucher'); 16593 if (voucherBtn) { 16594 if (voucherBtn.disabled || voucherBtn.getAttribute('data-stock-blocked') === 'true') { 16595 const stockReason = voucherBtn.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16596 16597 if (window.showToast) { 16598 window.showToast({ 16599 type: 'warn', 16600 title: 'Stok Tidak Tersedia', 16601 message: stockReason, 16602 duration: 3500 16603 }); 16604 } else { 16605 alert(stockReason); 16606 } 16607 return; 16608 } 16609 } 16610 16611 // Check login status first 16612 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16613 document.body.classList.contains('logged-in') || 16614 window.userLoggedIn === true || 16615 document.body.dataset.userLoggedIn === 'true'; 16616 16617 console.log('[buyNowWithVoucher] User logged in:', isLoggedIn); 16618 16619 if (!isLoggedIn) { 16620 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16621 window.location.href = '/login?return=' + currentUrl; 16622 return; 16623 } 16624 16625 // Get current product data 16626 const { productId, qty, variantId } = getCurrentProductData(); 16627 const payload = createBuyNowPayload(productId, qty, variantId); 16628 16629 console.log('[buyNowWithVoucher] Product data:', { productId, qty, variantId }); 16630 16631 // Get category path for voucher validation 16632 const categoryPath = window.PD_CATEGORY_PATH || ''; 16633 16634 // Step 1: Clear any existing voucher first (important for clean state) 16635 console.log('[buyNowWithVoucher] Step 1: Clearing existing voucher...'); 16636 try { 16637 await fetch('/api/voucher/remove.php', { 16638 method: 'POST', 16639 headers: { 16640 'X-CSRF-Token': getCsrfToken() 16641 }, 16642 body: JSON.stringify({ 16643 csrf_token: getCsrfToken() 16644 }), 16645 credentials: 'same-origin' 16646 }); 16647 } catch (e) { 16648 // Ignore error if no voucher exists 16649 console.debug('No existing voucher to remove or removal failed'); 16650 } 16651 16652 // Step 2: Apply voucher to session (like cart does) 16653 console.log('[buyNowWithVoucher] Step 2: Applying voucher to session...'); 16654 const applyRes = await fetch('/api/voucher/apply.php', { 16655 method: 'POST', 16656 headers: { 16657 'Content-Type': 'application/json', 16658 'X-CSRF-Token': getCsrfToken() 16659 }, 16660 credentials: 'same-origin', 16661 body: JSON.stringify({ 16662 csrf_token: getCsrfToken(), 16663 code: voucherCode, 16664 items: [{ 16665 pid: productId, 16666 price: payload.meta.price, 16667 qty: qty, 16668 category_path: categoryPath 16669 }], 16670 shipping_cost: null // Will be calculated in checkout 16671 }) 16672 }); 16673 16674 const applyData = await applyRes.json(); 16675 16676 console.log('[buyNowWithVoucher] Voucher apply response:', applyData); 16677 16678 if (!applyData.success) { 16679 // Voucher invalid - show error and fallback to regular buy now 16680 console.warn('[buyNowWithVoucher] Voucher application failed:', applyData.message); 16681 if (window.showToast) { 16682 window.showToast({ 16683 type: 'error', 16684 title: 'Voucher Tidak Valid', 16685 message: applyData.message || 'Voucher tidak dapat diterapkan untuk produk ini', 16686 duration: 4000 16687 }); 16688 } else { 16689 alert(applyData.message || 'Voucher tidak dapat diterapkan'); 16690 } 16691 16692 // Fallback to regular buy now without voucher 16693 console.log('Voucher failed, proceeding with regular buy now'); 16694 return window.buyNow(e); 16695 } 16696 16697 // Step 3: Store buy now data in sessionStorage 16698 console.log('[buyNowWithVoucher] Step 3: Storing buy now data in sessionStorage'); 16699 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16700 16701 // Step 4: Redirect to checkout 16702 // Voucher sudah di session, checkout akan auto-load via loadAppliedVoucher() 16703 console.log('[buyNowWithVoucher] Step 4: Redirecting to checkout with voucher applied'); 16704 window.location.href = '/checkout?mode=buy_now'; 16705 16706 } catch (err) { 16707 console.error('Buy Now with Voucher error:', err); 16708 16709 if (window.showToast) { 16710 window.showToast({ 16711 type: 'error', 16712 title: 'Terjadi Kesalahan', 16713 message: 'Gagal memproses pembelian dengan voucher. Silakan coba lagi.', 16714 duration: 3000 16715 }); 16716 } else { 16717 alert('Gagal memproses pembelian. Silakan coba lagi.'); 16718 } 16719 } 16720 }; 16721})(); 16722</script>
vendor: 1 bytes, line 16722
16722
16723<script data-inlined="pd-voucher-recalc.js"> 16724/** 16725 * Product Detail - Voucher Discount Recalculation 16726 * 16727 * Handles dynamic voucher discount recalculation when variant price changes 16728 * Supports both PERCENTAGE and FIXED_AMOUNT voucher types 16729 */ 16730 16731(function() { 16732 'use strict'; 16733 16734 /** 16735 * Calculate voucher discount based on type and price 16736 * 16737 * @param {string} type - Voucher type: 'PERCENTAGE' or 'FIXED_AMOUNT' 16738 * @param {number} value - Voucher value (percentage or fixed amount) 16739 * @param {number} price - Current variant price 16740 * @param {number} maxDiscount - Maximum discount for percentage vouchers (0 = no limit) 16741 * @return {number} Calculated discount amount 16742 */ 16743 function calculateVoucherDiscount(type, value, price, maxDiscount) { 16744 if (type === 'PERCENTAGE') { 16745 // Calculate percentage discount 16746 let discount = (price * value) / 100; 16747 16748 // Apply max discount cap if set 16749 if (maxDiscount > 0 && discount > maxDiscount) { 16750 discount = maxDiscount; 16751 } 16752 16753 return Math.round(discount); 16754 } else { 16755 // FIXED_AMOUNT - discount doesn't change with price 16756 return Math.round(value); 16757 } 16758 } 16759 16760 /** 16761 * Format number as Rupiah without 'Rp' prefix 16762 * 16763 * @param {number} num - Number to format 16764 * @return {string} Formatted number (e.g., "20.000") 16765 */ 16766 function formatNumber(num) { 16767 return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 16768 } 16769 16770 /** 16771 * Update voucher button discount label 16772 * 16773 * @param {number} newPrice - New variant price 16774 */ 16775 function updateVoucherDiscount(newPrice) { 16776 const voucherBtn = document.getElementById('btn-buy-voucher'); 16777 if (!voucherBtn) return; 16778 16779 // Get voucher data from button attributes 16780 const voucherType = voucherBtn.dataset.voucherType; 16781 const voucherValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 16782 const voucherMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 16783 16784 if (!voucherType || voucherValue === 0) return; 16785 16786 // Calculate new discount 16787 const newDiscount = calculateVoucherDiscount(voucherType, voucherValue, newPrice, voucherMax); 16788 16789 // Update the discount label 16790 const saveLabel = voucherBtn.querySelector('.bv-save'); 16791 if (saveLabel) { 16792 saveLabel.textContent = '-Rp' + formatNumber(newDiscount); 16793 } 16794 16795 // Also update mobile FAB voucher display if exists 16796 updateMobileFabVoucher(newDiscount); 16797 } 16798 16799 /** 16800 * Update mobile FAB voucher value 16801 * 16802 * @param {number} discountAmount - New discount amount 16803 */ 16804 function updateMobileFabVoucher(discountAmount) { 16805 const fab = document.getElementById('pd-mobile-fab'); 16806 if (fab) { 16807 fab.dataset.voucherValue = discountAmount.toString(); 16808 } 16809 } 16810 16811 /** 16812 * Listen to variant change events and recalculate discount 16813 */ 16814 function initVoucherRecalculation() { 16815 window.addEventListener('pd-variant-changed', function(e) { 16816 const detail = e.detail || {}; 16817 const newPrice = detail.price || 0; 16818 16819 if (newPrice > 0) { 16820 updateVoucherDiscount(newPrice); 16821 } 16822 }); 16823 } 16824 16825 // Initialize when DOM is ready 16826 if (document.readyState === 'loading') { 16827 document.addEventListener('DOMContentLoaded', initVoucherRecalculation); 16828 } else { 16829 initVoucherRecalculation(); 16830 } 16831 16832})(); 16833 16834</script>
vendor: 1 bytes, line 16834
16834
16835<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>
16835 16836
16837<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16838(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'}})}} 16839 16840function _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()}})(); 16841 16842(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()}})})(); 16843</script>
16843<footer class="site-footer footer-compact variant-stripes" role="contentinfo"> 16844 <div class="ft-top ft-collapsible" id="ftTop"> 16845 <div class="container ft-grid"> 16846 16847 <div class="ft-col ft-help"> 16848 <h4 class="ft-title">Butuh Bantuan?</h4> 16849 <p class="ft-desc">Hubungi kami melalui Official WhatsApp interaktif sekarang.</p> 16850 <a class="ft-wa-btn" href="https://mikale.me/whatsappBERLINTOTO" target="_blank" rel="nofollow noopener"> 16851 <span class="ico" aria-hidden="true"> 16852 <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> 16853 </span> 16854 Kirim Pesan 16855 </a> 16856 16857 <p class="ft-desc sm-gap">Ikuti kami:</p> 16858 <div class="ft-social"> 16859 <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> 16860 <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
16860V7.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> 16861 <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> 16862 </div> 16863 </div> 16864 16865 <!-- Collapsible Content Wrapper (Non-Homepage Only) --> 16866 <div class="ft-collapsible-content" id="ftCollapsible"> 16867 16868 <div class="ft-col"> 16869 <h4 class="ft-title">Tentang BERLINTOTO</h4> 16870 <ul class="ft-links"> 16871 <li><a href="https://www.bim.miami/">Tentang BERLINTOTO</a></li> 16872 <li><a href="https://www.bim.miami/">Kontak BERLINTOTO</a></li> 16873 <li><a href="https://www.bim.miami/">Lokasi Toko</a></li> 16874 <li><a href="https://www.bim.miami/">FAQ</a></li> 16875 </ul> 16876 <div class="ft-address"> 16877 <p class="company">COSTEL</p> 16878 <p>JL. Harapan Blok Melati No. 27, Kel. Pademangan Barat,</p> 16879 <p>Kec. Jakarta, Kota Jakarta, DKI JAKARTA, 11752</p> 16880 </div> 16881 </div> 16882 16883 <div class="ft-col"> 16884 <h4 class="ft-title">Informasi</h4> 16885 <ul class="ft-links"> 16886 <li><a href="/syarat-dan-ketentuan">Syarat & Ketentuan</a></li> 16887 <li><a href="/kebijakan-privasi">Kebijakan Privasi</a></li> 16888 <li><a href="/kebijakan-cookie">Kebijakan Cookie</a></li> 16889 <li><a href="/pengiriman-dan-pengembalian">Pengiriman & Pengembalian</a></li> 16890 <li><a href="/pembayaran-dan-cicilan">Pembayaran & Cicilan</a></li> 16891 <li><a href="/garansi-dan-layanan">Garansi & Layanan</a></li> 16892 </ul> 16893 16894 <div class="ft-payments payments-small" aria-label="Metode Pembayaran"> 16895 <div class="pay-row"> 16896 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-mandiri.png" alt="Mandiri" loading="lazy"> 16897 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bni.gif" alt="BNI" loading="lazy"> 16898 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bri.gif" alt="BRI" loading="lazy"> 16899 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-qris.gif" alt="QRIS" loading="lazy"> 16900 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bca.gif" alt="BCA" loading="lazy"> 16901 </div> 16902 </div> 16903 </div> 16904 16905 </div> 16906 <!-- End Collapsible Content --> 16907 16908 <!-- Expand Button (Mobile Only) --> 16909 <button class="ft-expand-btn" id="ftExpandBtn" aria-label="Lihat semua informasi footer"> 16910 <span class="expand-text">Selengkapnya</span> 16911 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 16912 <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> 16913 </svg> 16914 </button> 16915 16916 </div> 16917 </div> 16918 <div class="ft-bottom"> 16919 <div class="container ft-bottom-inner"> 16920 <div class="ft-copy"> 16921 <span>ANGIN-RIBUT © <span id="yearNow">2026</span></span> 16922 <span>PT ANGIN-RIBUT. All rights reserved.</span> 16923 </div> 16924 </div> 16925 </div> 16926 16927 <style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16928 /* Chat Widget Styles - Lightweight & Minimal */ 16929 .chat-widget { 16930 position: fixed; 16931 bottom: 20px; 16932 right: 20px; 16933 z-index: 9999; 16934 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 16935 } 16936 16937 /* Hide floating button on mobile - use bottom nav instead */ 16938 @media (max-width: 640px) { 16939 .chat-toggle-btn { 16940 display: none !important; 16941 } 16942 16943 /* Ensure popup is still visible on mobile even when button is hidden */ 16944 .chat-widget { 16945 display: block !important; 16946 } 16947 16948 .chat-popup { 16949 display: none; /* Hidden by default, shown via JS */ 16950 } 16951 16952 .chat-popup[style*="display: block"], 16953 .chat-popup[style*="display:block"] { 16954 display: block !important; 16955 } 16956 } 16957 16958 .chat-toggle-btn { 16959 width: 60px; 16960 height: 60px; 16961 border-radius: 50%; 16962 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 16963 border: none; 16964 color: white; 16965 cursor: pointer; 16966 box-shadow: 0 4px 12px rgba(242, 140, 26, 0.3); 16967 position: relative; 16968 transition: transform 0.2s, box-shadow 0.2s; 16969 } 16970 16971 .chat-toggle-btn:hover { 16972 transform: scale(1.05); 16973 box-shadow: 0 6px 20px rgba(242, 140, 26, 0.4); 16974 } 16975 16976 #chat-toggle-btn .chat-icon { 16977 position: absolute; 16978 top: 50%; 16979 left: 50%; 16980 transform: translate(-50%, -50%); 16981 width: 28px !important; 16982 height: 28px !important; 16983 } 16984 16985 .chat-unread-badge { 16986 position: absolute; 16987 top: -5px; 16988 right: -5px; 16989 background: #ef4444; 16990 color: white; 16991 border-radius: 12px; 16992 padding: 2px 6px; 16993 font-size: 11px; 16994 font-weight: 600;
16995 min-width: 20px; 16996 text-align: center; 16997 } 16998 16999 .chat-popup { 17000 position: absolute; 17001 bottom: 80px; 17002 right: 0; 17003 width: 380px; 17004 max-width: calc(100vw - 40px); 17005 height: 500px; 17006 max-height: calc(100vh - 120px); 17007 background: white; 17008 border-radius: 12px; 17009 box-shadow: 0 10px 40px rgba(0,0,0,0.2); 17010 display: flex; 17011 flex-direction: column; 17012 overflow: hidden; 17013 } 17014 17015 .chat-popup-header { 17016 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17017 color: white; 17018 padding: 16px 20px; 17019 display: flex; 17020 justify-content: space-between; 17021 align-items: center; 17022 flex-shrink: 0; 17023 } 17024 17025 .chat-header-title { 17026 display: flex; 17027 flex-direction: column; 17028 gap: 2px; 17029 min-height: 0; 17030 } 17031 17032 .chat-popup-header h3 { 17033 margin: 0; 17034 font-size: 18px; 17035 font-weight: 600; 17036 line-height: 1.2; 17037 } 17038 17039 .chat-operating-hours { 17040 font-size: 11px; 17041 opacity: 0.9; 17042 font-weight: 400; 17043 display: flex; 17044 align-items: center; 17045 gap: 4px; 17046 line-height: 1.2; 17047 } 17048 17049 .chat-operating-hours svg { 17050 width: 12px; 17051 height: 12px; 17052 opacity: 0.8; 17053 flex-shrink: 0; 17054 } 17055 17056 .chat-header-actions { 17057 display: flex; 17058 align-items: center; 17059 gap: 8px; 17060 } 17061 17062 /* Notification Toggle Button */ 17063 .btn-notif-toggle { 17064 background: none; 17065 border: none; 17066 color: white; 17067 font-size: 18px; 17068 cursor: pointer; 17069 padding: 4px 8px; 17070 transition: all 0.2s; 17071 opacity: 0.8; 17072 } 17073 17074 .btn-notif-toggle:hover { 17075 opacity: 1; 17076 transform: scale(1.1); 17077 } 17078 17079 /* Icon states */ 17080 .btn-notif-toggle[data-status="not-subscribed"] svg { 17081 opacity: 0.7; 17082 } 17083 17084 .btn-notif-toggle[data-status="not-subscribed"]:hover svg { 17085 opacity: 1; 17086 } 17087 17088 .btn-notif-toggle[data-status="subscribed"] svg { 17089 opacity: 1; 17090 animation: bellRing 2s ease-in-out; 17091 } 17092 17093 .btn-notif-toggle[data-status="blocked"] svg { 17094 color: #ff6b6b; 17095 } 17096 17097 .btn-notif-toggle[data-status="not-supported"] { 17098 display: none !important; 17099 } 17100 17101 @keyframes bellRing { 17102 0%, 100% { transform: rotate(0deg); } 17103 10%, 30% { transform: rotate(-10deg); } 17104 20%, 40% { transform: rotate(10deg); } 17105 } 17106 17107 /* Notification Info Bar */ 17108 .chat-notif-info-bar { 17109 position: absolute; 17110 top: 64px; 17111 left: 0; 17112 right: 0; 17113 background: linear-gradient(to right, #e3f2fd, #f5f5f5); 17114 border-bottom: 1px solid #bbdefb; 17115 padding: 8px 12px; 17116 display: flex; 17117 align-items: center; 17118 gap: 8px; 17119 font-size: 12px; 17120 color: #616161; 17121 z-index: 10; 17122 animation: slideDown 0.3s ease; 17123 } 17124 17125 .chat-notif-info-bar svg { 17126 flex-shrink: 0; 17127 } 17128 17129 .chat-notif-info-bar .small { 17130 flex: 1; 17131 line-height: 1.4; 17132 } 17133 17134 .chat-notif-info-bar .btn-dismiss { 17135 background: none; 17136 border: none; 17137 color: #9e9e9e; 17138 cursor: pointer; 17139 padding: 0 4px; 17140 font-size: 16px; 17141 transition: color 0.2s; 17142 } 17143 17144 .chat-notif-info-bar .btn-dismiss:hover { 17145 color: #616161; 17146 } 17147 17148 @keyframes slideDown { 17149 from { 17150 max-height: 0; 17151 opacity: 0; 17152 } 17153 to { 17154 max-height: 50px; 17155 opacity: 1; 17156 } 17157 } 17158 17159 .chat-close-btn { 17160 background: none; 17161 border: none; 17162 color: white; 17163 font-size: 28px; 17164 line-height: 1; 17165 cursor: pointer; 17166 padding: 0; 17167 width: 30px; 17168 height: 30px; 17169 } 17170 17171 .chat-popup-body { 17172 flex: 1 1 auto; 17173 display: flex; 17174 flex-direction: column; 17175 min-height: 0; 17176 overflow: hidden; 17177 height: calc(100% - 64px); 17178 position: relative; 17179 transition: padding-top 0.3s ease; 17180 } 17181 17182 /* Add padding when info bar is visible */ 17183 .chat-popup-body.has-info-bar { 17184 padding-top: 40px; 17185 } 17186 17187 .chat-view { 17188 flex: 1 1 auto; 17189 display: none; 17190 flex-direction: column; 17191 min-height: 0; 17192 overflow: hidden; 17193 } 17194 17195 #chat-list-view { 17196 display: flex; 17197 } 17198 17199 #chat-messages-view { 17200 display: none; 17201 } 17202 17203 #chat-messages-view.active { 17204 display: flex; 17205 flex-direction: column; 17206 } 17207 17208 #chat-new-view { 17209 display: none; 17210 } 17211 17212 #chat-new-view.active { 17213 display: flex; 17214 } 17215 17216 .chat-conversations-list { 17217 flex: 1; 17218 overflow-y: auto; 17219 padding: 0; 17220 background: white; 17221 } 17222 17223 /* Custom Scrollbar for Conversations List */ 17224 .chat-conversations-list::-webkit-scrollbar {
17225 width: 6px; 17226 } 17227 17228 .chat-conversations-list::-webkit-scrollbar-track { 17229 background: #f1f1f1; 17230 } 17231 17232 .chat-conversations-list::-webkit-scrollbar-thumb { 17233 background: #F28C1A; 17234 border-radius: 3px; 17235 } 17236 17237 .chat-conversations-list::-webkit-scrollbar-thumb:hover { 17238 background: #D97809; 17239 } 17240 17241 /* Firefox */ 17242 .chat-conversations-list { 17243 scrollbar-width: thin; 17244 scrollbar-color: #F28C1A #f1f1f1; 17245 } 17246 17247 .chat-conversation-item { 17248 padding: 12px 16px; 17249 cursor: pointer; 17250 display: flex; 17251 gap: 12px; 17252 align-items: flex-start; 17253 transition: background 0.15s ease; 17254 color: #33371f; 17255 border-bottom: 1px solid #f0f0f0; 17256 } 17257 17258 .chat-conversation-item:hover { 17259 background: #f5f5f5; 17260 } 17261 17262 .chat-conversation-item.active { 17263 background: #FFF5EB; 17264 } 17265 17266 /* WhatsApp-like Avatar */ 17267 .conversation-avatar { 17268 width: 48px; 17269 height: 48px; 17270 border-radius: 50%; 17271 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17272 flex-shrink: 0; 17273 display: flex; 17274 align-items: center; 17275 justify-content: center; 17276 color: white; 17277 font-weight: 600; 17278 font-size: 18px; 17279 } 17280 17281 .conversation-avatar svg { 17282 width: 24px; 17283 height: 24px; 17284 } 17285 17286 /* WhatsApp-like Content */ 17287 .conversation-content { 17288 flex: 1; 17289 min-width: 0; 17290 display: flex; 17291 flex-direction: column; 17292 gap: 4px; 17293 } 17294 17295 .conversation-header { 17296 display: flex; 17297 justify-content: space-between; 17298 align-items: center; 17299 gap: 8px; 17300 } 17301 17302 .conversation-name { 17303 font-weight: 600; 17304 font-size: 15px; 17305 color: #111; 17306 white-space: nowrap; 17307 overflow: hidden; 17308 text-overflow: ellipsis; 17309 } 17310 17311 .conversation-time { 17312 font-size: 12px; 17313 color: #7d8166; 17314 flex-shrink: 0; 17315 } 17316 17317 .conversation-preview { 17318 display: flex; 17319 justify-content: space-between; 17320 align-items: center; 17321 gap: 8px; 17322 } 17323 17324 .conversation-message { 17325 font-size: 14px; 17326 color: #667781; 17327 white-space: nowrap; 17328 overflow: hidden; 17329 text-overflow: ellipsis; 17330 margin: 0; 17331 flex: 1; 17332 } 17333 17334 .conversation-unread { 17335 background: #25D366; 17336 color: white; 17337 border-radius: 12px; 17338 padding: 2px 8px; 17339 font-size: 12px; 17340 font-weight: 600; 17341 min-width: 20px; 17342 text-align: center; 17343 flex-shrink: 0; 17344 } 17345 17346 /* Active item styling */ 17347 .chat-conversation-item.active .conversation-name { 17348 color: #F28C1A; 17349 } 17350 17351 .chat-conversation-item.active .conversation-time { 17352 color: #F28C1A; 17353 } 17354 17355 .chat-new-btn { 17356 margin: 12px; 17357 padding: 12px; 17358 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17359 color: white; 17360 border: none; 17361 border-radius: 8px; 17362 cursor: pointer; 17363 font-weight: 500; 17364 font-size: 14px; 17365 display: flex; 17366 align-items: center; 17367 justify-content: center; 17368 gap: 8px; 17369 transition: all 0.2s; 17370 flex-shrink: 0; 17371 } 17372 17373 .chat-new-btn:hover { 17374 transform: translateY(-1px); 17375 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17376 } 17377 17378 .chat-conversation-title { 17379 flex: 1; 17380 font-weight: 600; 17381 font-size: 16px; 17382 color: #1f2937; 17383 } 17384 17385 .chat-messages-container { 17386 flex: 1; 17387 overflow-y: auto; 17388 padding: 16px; 17389 display: flex; 17390 flex-direction: column; 17391 gap: 8px; 17392 min-height: 0; 17393 background: #E5DDD5; 17394 } 17395 17396 /* Custom Scrollbar for Messages Container */ 17397 .chat-messages-container::-webkit-scrollbar {
17398 width: 6px; 17399 } 17400 17401 .chat-messages-container::-webkit-scrollbar-track { 17402 background: rgba(0, 0, 0, 0.05); 17403 } 17404 17405 .chat-messages-container::-webkit-scrollbar-thumb { 17406 background: rgba(242, 140, 26, 0.6); 17407 border-radius: 3px; 17408 } 17409 17410 .chat-messages-container::-webkit-scrollbar-thumb:hover { 17411 background: #F28C1A; 17412 } 17413 17414 /* Firefox */ 17415 .chat-messages-container { 17416 scrollbar-width: thin; 17417 scrollbar-color: rgba(242, 140, 26, 0.6) rgba(0, 0, 0, 0.05); 17418 } 17419 17420 /* WhatsApp Group-like Messages */ 17421 .chat-message { 17422 display: flex; 17423 margin-bottom: 4px; 17424 animation: fadeIn 0.2s ease; 17425 } 17426 17427 @keyframes fadeIn { 17428 from { opacity: 0; transform: translateY(10px); } 17429 to { opacity: 1; transform: translateY(0); } 17430 } 17431 17432 .chat-message.mine { 17433 justify-content: flex-end; 17434 } 17435 17436 .chat-message-bubble { 17437 max-width: 80%; 17438 padding: 6px 8px 8px 10px; 17439 border-radius: 8px; 17440 position: relative; 17441 box-shadow: 0 1px 0.5px rgba(0,0,0,0.13); 17442 } 17443 17444 /* Bubble from others (Admin/CS) */ 17445 .chat-message:not(.mine) .chat-message-bubble { 17446 background: #FFFFFF; 17447 border-radius: 0 8px 8px 8px; 17448 } 17449 17450 /* Bubble tail for others */ 17451 .chat-message:not(.mine) .chat-message-bubble::before { 17452 content: ''; 17453 position: absolute; 17454 top: 0; 17455 left: -8px; 17456 width: 0; 17457 height: 0; 17458 border-style: solid; 17459 border-width: 0 8px 13px 0; 17460 border-color: transparent #FFFFFF transparent transparent; 17461 } 17462 17463 /* Bubble from user (mine) */ 17464 .chat-message.mine .chat-message-bubble { 17465 background: #e4f5d0; /* Light blue for user messages */ 17466 border-radius: 8px 0 8px 8px; 17467 } 17468 17469 /* Bubble tail for mine */ 17470 .chat-message.mine .chat-message-bubble::before { 17471 content: ''; 17472 position: absolute; 17473 top: 0; 17474 right: -8px; 17475 width: 0; 17476 height: 0; 17477 border-style: solid; 17478 border-width: 0 0 13px 8px; 17479 border-color: transparent transparent transparent #eaf5d0; 17480 } 17481 17482 .message-sender { 17483 font-size: 12.5px; 17484 font-weight: 600; 17485 color: #F28C1A !important; 17486 margin-bottom: 4px; 17487 } 17488 17489 .chat-message.mine .message-sender { 17490 display: none; 17491 } 17492 17493 /* Message text */ 17494 .message-text { 17495 font-size: 14px; 17496 line-height: 19px; 17497 color: #ffffff !important; 17498 word-wrap: break-word; 17499 white-space: pre-wrap; 17500 margin-bottom: 4px; 17501 } 17502 17503 /* Message metadata (time + status) */ 17504 .message-meta { 17505 display: flex; 17506 align-items: center; 17507 justify-content: flex-end; 17508 gap: 4px; 17509 margin-top: 2px; 17510 } 17511 17512 .message-time { 17513 font-size: 11px; 17514 color: #728166 !important; 17515 line-height: 15px; 17516 } 17517 17518 /* Use multiple selectors to override global styles */ 17519 .chat-message-bubble .message-status, 17520 .message-meta .message-status, 17521 .chat-message .message-status { 17522 width: 16px !important; 17523 height: 16px !important; 17524 min-width: 16px !important; 17525 min-height: 16px !important; 17526 max-width: 16px !important; 17527 max-height: 16px !important; 17528 flex-shrink: 0; 17529 color: #728166 !important; 17530 fill: currentColor !important; 17531 } 17532 17533 .chat-message:not(.mine) .message-status { 17534 display: none; 17535 } 17536 17537 /* Loading dots animation */ 17538 @keyframes loadingDots { 17539 0%, 20% { content: '.'; } 17540 40% { content: '..'; } 17541 60%, 100% { content: '...'; } 17542 } 17543 17544 .message-status.loading::after { 17545 content: '...'; 17546 animation: loadingDots 1.5s infinite; 17547 font-size: 16px; 17548 line-height: 16px; 17549 } 17550 17551 .message-status.loading path { 17552 display: none; 17553 } 17554 17555 .message-status.failed { 17556 color: #ef4444 !important; 17557 cursor: pointer; 17558 } 17559 17560 .message-status.sent { 17561 color: #667781 !important; 17562 } 17563 17564 .chat-new-form { 17565 flex: 1; 17566 overflow-y: auto; 17567 padding: 16px; 17568 display: flex; 17569 flex-direction: column; 17570 } 17571 17572 .chat-new-form form { 17573 display: flex; 17574 flex-direction: column; 17575 gap: 16px; 17576 flex: 1; 17577 } 17578 17579 .chat-new-form .form-group { 17580 display: flex; 17581 flex-direction: column; 17582 gap: 6px; 17583 } 17584 17585 .chat-new-form label { 17586 font-size: 14px; 17587 font-weight: 500; 17588 color: #374151; 17589 } 17590 17591 .chat-new-form .form-control { 17592 width: 100%; 17593 padding: 10px 12px; 17594 border: 1px solid #d1d5db; 17595 border-radius: 8px; 17596 font-size: 14px; 17597 font-family: inherit; 17598 transition: border-color 0.2s; 17599 box-sizing: border-box; 17600 } 17601 17602 .chat-new-form .form-control:focus { 17603 outline: none; 17604 border-color: #F28C1A; 17605 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17606 } 17607 17608 .chat-new-form textarea.form-control { 17609 resize: vertical; 17610 min-height: 100px; 17611 } 17612 17613 .chat-new-form .btn { 17614 padding: 12px 20px; 17615 border: none; 17616 border-radius: 8px; 17617 font-size: 15px; 17618 font-weight: 500; 17619 cursor: pointer; 17620 transition: all 0.2s; 17621 text-align: center; 17622 } 17623 17624 .chat-new-form .btn-primary { 17625 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17626 color: white; 17627 } 17628 17629 .chat-new-form .btn-primary:hover { 17630 transform: translateY(-1px); 17631 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17632 } 17633 17634 .chat-new-form .btn-primary:active { 17635 transform: scale(0.98); 17636 } 17637 17638 .chat-new-form .btn-block { 17639 width: 100%; 17640 margin-top: auto; 17641 } 17642 17643 .chat-message-form { 17644 padding: 12px; 17645 border-top: 1px solid #e5e7eb; 17646 display: flex; 17647 flex-wrap: wrap; 17648 gap: 8px; 17649 align-items: flex-end; 17650 flex-shrink: 0; 17651 background: white; 17652 } 17653 17654 .chat-product-card { 17655 flex: 1 1 100%; 17656 max-width: 100%; 17657 display: flex; 17658 gap: 12px; 17659 padding: 12px; 17660 background: #f3f4f6; 17661 border-radius: 8px; 17662 margin-bottom: 4px; 17663 border: 1px solid #e5e7eb; 17664 } 17665 17666 .chat-product-card img { 17667 width: 60px; 17668 height: 60px; 17669 object-fit: cover; 17670 border-radius: 6px; 17671 flex-shrink: 0; 17672 } 17673 17674 .chat-product-card-content { 17675 flex: 1; 17676 min-width: 0; 17677 } 17678 17679 .chat-product-card-name { 17680 font-weight: 500; 17681 font-size: 14px; 17682 color: #1f2937; 17683 margin-bottom: 4px; 17684 white-space: nowrap; 17685 overflow: hidden; 17686 text-overflow: ellipsis; 17687 } 17688 17689 .chat-product-card-variant { 17690 font-size: 12px; 17691 color: #74806b; 17692 margin-bottom: 4px; 17693 } 17694 17695 .chat-product-card-price { 17696 font-size: 13px; 17697 color: #8bea66; 17698 font-weight: 600; 17699 } 17700 17701 .chat-product-card-remove { 17702 align-self: flex-start; 17703 background: none; 17704 border: none; 17705 color: #9ca3af; 17706 cursor: pointer; 17707 padding: 4px; 17708 font-size: 18px; 17709 line-height: 1; 17710 transition: color 0.2s; 17711 } 17712 17713 .chat-product-card-remove:hover { 17714 color: #ef4444; 17715 } 17716 17717 /* Product card in messages (read-only display) */ 17718 .message-product-card { 17719 display: flex; 17720 gap: 10px; 17721 padding: 10px; 17722 background: rgba(255, 255, 255, 0.5); 17723 border-radius: 8px; 17724 margin-bottom: 6px; 17725 border: 1px solid rgba(0, 0, 0, 0.08); 17726 align-items: center; 17727 } 17728 17729 .chat-message.mine .message-product-card { 17730 background: rgba(255, 255, 255, 0.7); 17731 } 17732 17733 .message-product-card img { 17734 width: 50px; 17735 height: 50px; 17736 object-fit: cover; 17737 border-radius: 6px; 17738 flex-shrink: 0; 17739 } 17740 17741 .message-product-info { 17742 flex: 1; 17743 min-width: 0; 17744 } 17745 17746 .message-product-name { 17747 font-weight: 500; 17748 font-size: 13px; 17749 color: #33371f; 17750 margin-bottom: 2px; 17751 overflow: hidden; 17752 text-overflow: ellipsis; 17753 display: -webkit-box; 17754 -webkit-line-clamp: 2; 17755 -webkit-box-orient: vertical; 17756 line-height: 1.3; 17757 } 17758 17759 .message-product-variant { 17760 font-size: 11px; 17761 color: #78806b; 17762 margin-bottom: 2px; 17763 } 17764 17765 .message-product-price { 17766 font-size: 12px; 17767 color: #c7ea66; 17768 font-weight: 600; 17769 } 17770 17771 .message-product-link { 17772 flex-shrink: 0; 17773 width: 32px; 17774 height: 32px; 17775 display: flex; 17776 align-items: center; 17777 justify-content: center; 17778 background: rgba(102, 126, 234, 0.1); 17779 border-radius: 6px; 17780 color: #c7ea66; 17781 text-decoration: none; 17782 transition: all 0.2s; 17783 } 17784 17785 .message-product-link:hover { 17786 background: #b7ea66; 17787 color: white; 17788 } 17789 17790 .message-product-link svg { 17791 width: 16px; 17792 height: 16px; 17793 } 17794 17795 .chat-message-input { 17796 flex: 1; 17797 border: 1px solid #d1d5db; 17798 border-radius: 20px; 17799 padding: 10px 16px; 17800 resize: none; 17801 max-height: 100px; 17802 font-family: inherit; 17803 font-size: 14px; 17804 line-height: 1.5; 17805 box-sizing: border-box; 17806 overflow-y: hidden; 17807 } 17808 17809 .chat-message-input:focus { 17810 outline: none; 17811 border-color: #F28C1A; 17812 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17813 } 17814 17815 .chat-send-btn { 17816 width: 40px; 17817 height: 40px; 17818 border-radius: 50%; 17819 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17820 border: none; 17821 color: white; 17822 cursor: pointer; 17823 flex-shrink: 0; 17824 display: flex; 17825 align-items: center; 17826 justify-content: center; 17827 transition: all 0.2s; 17828 } 17829 17830 .chat-send-btn:hover { 17831 transform: scale(1.05); 17832 box-shadow: 0 2px 8px rgba(242, 140, 26, 0.3); 17833 } 17834 17835 .chat-send-btn:active { 17836 transform: scale(0.95); 17837 } 17838 17839 .chat-loading { 17840 text-align: center; 17841 padding: 20px; 17842 color: #6b7280; 17843 } 17844 17845 /* Date Separator (WhatsApp-style) */ 17846 .date-separator { 17847 display: flex; 17848 align-items: center; 17849 gap: 12px; 17850 margin: 24px 0 16px; 17851 opacity: 0; 17852 animation: fadeInSeparator 0.3s ease forwards; 17853 } 17854 17855 @keyframes fadeInSeparator { 17856 from { 17857 opacity: 0; 17858 transform: translateY(-10px); 17859 } 17860 to { 17861 opacity: 1; 17862 transform: translateY(0); 17863 } 17864 } 17865 17866 .date-separator-line { 17867 flex: 1; 17868 height: 1px; 17869 background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1) 50%, transparent); 17870 } 17871 17872 .date-separator-text { 17873 flex-shrink: 0; 17874 padding: 4px 12px; 17875 background: #e9ecef; 17876 color: #6c757d; 17877 font-size: 11px; 17878 font-weight: 600; 17879 text-transform: uppercase; 17880 letter-spacing: 0.5px; 17881 border-radius: 12px; 17882 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 17883 } 17884 17885 /* Mobile responsive */ 17886 @media (max-width: 640px) { 17887 .chat-popup { 17888 position: fixed !important; /* Override absolute positioning */ 17889 bottom: 45px !important; /* Above bottom nav */ 17890 right: 0 !important; 17891 left: 0 !important; 17892 width: 100% !important; 17893 max-width: 100% !important; 17894 /* Use dvh (dynamic viewport height) for browser UI awareness */ 17895 height: calc(100dvh - 45px) !important; 17896 max-height: calc(100dvh - 45px) !important; 17897 /* Fallback for browsers that don't support dvh */ 17898 height: calc(var(--viewport-height, 100vh) - 45px) !important; 17899 max-height: calc(var(--viewport-height, 100vh) - 45px) !important; 17900 border-radius: 0 !important; 17901 z-index: 9999 !important; 17902 } 17903 17904 .chat-popup-header { 17905 border-radius: 0; 17906 } 17907
17908 .chat-messages-container, 17909 .chat-conversations-list { 17910 /* Adjust for mobile keyboard using dynamic viewport */ 17911 max-height: calc(100dvh - 190px); 17912 /* Fallback for browsers that don't support dvh */ 17913 max-height: calc(var(--viewport-height, 100vh) - 190px); 17914 } 17915 } 17916 </style> 17917 <!-- Chat Widget Button --> 17918 <div id="chat-widget" class="chat-widget" 17919 data-user-id="" 17920 data-is-logged-in="false" 17921 data-csrf-token="df5d776748b1ed8c8dd0d987bae99b1ee3dea6c6dcbe0eedb1bea766c56d569a"> 17922 <button id="chat-toggle-btn" class="chat-toggle-btn" aria-label="Toggle chat"> 17923 <svg class="chat-icon" width="28" height="28" viewBox="0 0 640 640" fill="currentColor"> 17924 <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"/> 17925 </svg> 17926 <span id="chat-unread-badge" class="chat-unread-badge" style="display: none;">0</span> 17927 </button> 17928 17929 <div id="chat-popup" class="chat-popup" style="display: none;"> 17930 <div class="chat-popup-header"> 17931 <div class="chat-header-title"> 17932 <h3>Bantuan</h3> 17933 <span id="chatOperatingHours" class="chat-operating-hours" style="display: none;"></span> 17934 </div> 17935 <div class="chat-header-actions"> 17936 <button id="chatNotifToggle" class="btn-notif-toggle" data-status="not-supported" title="Push notification" style="display: none;"> 17937 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 17938 <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"/> 17939 </svg> 17940 </button> 17941 <button id="chat-close-btn" class="chat-close-btn" aria-label="Close chat">Ã</button> 17942 </div> 17943 </div> 17944 17945 <!-- Notification Info Bar (conditional) --> 17946 <div class="chat-notif-info-bar" id="chatNotifInfoBar" style="display:none;"> 17947 <svg width="14" height="14" viewBox="0 0 24 24" fill="#ffc107"> 17948 <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"/> 17949 </svg> 17950 <span class="small">Aktifkan notifikasi untuk pemberitahuan balasan</span> 17951 <button class="btn-dismiss" onclick="ChatNotification.dismissInfo()">â</button> 17952 </div> 17953 17954 <div class="chat-popup-body"> 17955 <!-- Conversations List View --> 17956 <div id="chat-list-view" class="chat-view"> 17957 <div id="chat-conversations-list" class="chat-conversations-list"> 17958 <div class="chat-loading">Memuat...</div> 17959 <!-- Conversation Item Structure (rendered by JS): 17960 <div class="chat-conversation-item"> 17961 <div class="conversation-avatar"> 17962 <svg>...</svg> 17963 </div> 17964 <div class="conversation-content"> 17965 <div class="conversation-header">
17966 <span class="conversation-name">Admin Support</span> 17967 <span class="conversation-time">10:30</span> 17968 </div> 17969 <div class="conversation-preview"> 17970 <p class="conversation-message">Terima kasih telah menghubungi kami...</p> 17971 <span class="conversation-unread">2</span> 17972 </div> 17973 </div> 17974 </div> 17975 --> 17976 </div> 17977 <button id="chat-new-btn" class="chat-new-btn"> 17978 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17979 <line x1="12" y1="5" x2="12" y2="19"></line> 17980 <line x1="5" y1="12" x2="19" y2="12"></line> 17981 </svg> 17982 Percakapan Baru 17983 </button> 17984 </div> 17985 17986 <!-- Chat Messages View --> 17987 <div id="chat-messages-view" class="chat-view" style="display: none;"> 17988 <div id="chat-messages-container" class="chat-messages-container"> 17989 <div class="chat-loading">Memuat pesan...</div> 17990 <!-- Message Structure (rendered by JS): 17991 <div class="chat-message"> (or .chat-message.mine) 17992 <div class="chat-message-bubble"> 17993 <div class="message-sender">Admin Support</div> (only for non-mine) 17994 <div class="message-text">Halo, ada yang bisa dibantu?</div> 17995 <div class="message-meta"> 17996 <span class="message-time">10:30</span> 17997 <svg class="message-status">...</svg> (only for mine) 17998 </div> 17999 </div> 18000 </div> 18001 --> 18002 </div> 18003 18004 <form id="chat-message-form" class="chat-message-form"> 18005 <input type="hidden" id="chat-current-conversation-id" value=""> 18006 <textarea 18007 id="chat-message-input" 18008 class="chat-message-input" 18009 placeholder="Ketik pesan..." 18010 rows="1" 18011 maxlength="5000" 18012 ></textarea> 18013 <button type="submit" class="chat-send-btn" aria-label="Send message"> 18014 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18015 <line x1="22" y1="2" x2="11" y2="13"></line> 18016 <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 18017 </svg> 18018 </button> 18019 </form> 18020 </div> 18021 18022 <!-- New Chat View --> 18023 <div id="chat-new-view" class="chat-view" style="display: none;"> 18024 <div class="chat-messages-header"> 18025 <button id="chat-new-back-btn" class="chat-back-btn" aria-label="Back to list"> 18026 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18027 <path d="M19 12H5M12 19l-7-7 7-7"/> 18028 </svg> 18029 </button> 18030 <div class="chat-conversation-title">Percakapan Baru</div> 18031 </div> 18032 18033 <div class="chat-new-form"> 18034 <form id="chat-start-form"> 18035 <div class="form-group"> 18036 <label for="chat-new-subject">Subjek</label> 18037 <input 18038 type="text" 18039 id="chat-new-subject" 18040 class="form-control" 18041 placeholder="Bantuan Pelanggan" 18042 maxlength="255" 18043 > 18044 </div> 18045 <div class="form-group"> 18046 <label for="chat-new-message">Pesan</label> 18047 <textarea 18048 id="chat-new-message" 18049 class="form-control" 18050 rows="4" 18051 placeholder="Tulis pertanyaan Anda..." 18052 required 18053 maxlength="5000" 18054 ></textarea> 18055 </div> 18056 <button type="submit" class="btn btn-primary btn-block">Kirim</button> 18057 </form> 18058 </div> 18059 </div> 18060 </div> 18061 </div> 18062 </div> 18063 18064 <!-- Login Required Modal --> 18065 <div class="modal-overlay" id="chatLoginModal"> 18066 <div class="modal-dialog"> 18067 <div class="modal-header"> 18068 <h3 class="modal-title"> 18069 <div class="modal-icon info"> 18070 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18071 <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"/> 18072 </svg> 18073 </div> 18074 Login Diperlukan 18075 </h3> 18076 </div> 18077 <div class="modal-body"> 18078 <p class="modal-message">
18078Anda harus login terlebih dahulu untuk menggunakan fitur chat bantuan pelanggan.</p> 18079 <div class="modal-actions"> 18080 <button class="modal-btn modal-btn-secondary" id="chatLoginCancel">Batal</button> 18081 <button class="modal-btn modal-btn-primary" id="chatLoginConfirm">Login Sekarang</button> 18082 </div> 18083 </div> 18084 </div> 18085 </div> 18086
18086<script src="https://plazait.co.id/assets/js/chat-widget.js" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script>
18086<script data-inlined="chat-notification.js"> 18087/** 18088 * Customer Chat Push Notification Manager 18089 * Handles browser push notification subscription for customers 18090 * Version: 1.0.0 18091 * Date: 2026-01-19 18092 */ 18093 18094const ChatNotification = { 18095 // State 18096 status: 'not-supported', // not-supported, not-subscribed, subscribed, blocked 18097 subscription: null, 18098 vapidPublicKey: null, // Will be loaded from server 18099 18100 /** 18101 * Initialize notification manager 18102 */ 18103 async init() { 18104 // Check if user is logged in (only for logged-in users) 18105 if (!this.isUserLoggedIn()) { 18106 this.updateUI('not-supported'); 18107 return; 18108 } 18109 18110 // Check browser support 18111 if (!this.isSupported()) { 18112 this.updateUI('not-supported'); 18113 return; 18114 } 18115 18116 // Load VAPID key from server 18117 await this.loadVapidKey(); 18118 18119 if (!this.vapidPublicKey) { 18120 this.updateUI('not-supported'); 18121 return; 18122 } 18123 18124 // Register service worker if needed 18125 await this.registerServiceWorker(); 18126 18127 // Check current permission 18128 const permission = Notification.permission; 18129 18130 if (permission === 'denied') { 18131 this.updateUI('blocked'); 18132 } else if (permission === 'granted') { 18133 // Check if already subscribed 18134 const isSubscribed = await this.checkSubscriptionStatus(); 18135 this.updateUI(isSubscribed ? 'subscribed' : 'not-subscribed'); 18136 } else { 18137 // Default: not subscribed yet 18138 this.updateUI('not-subscribed'); 18139 this.showInfoBar(); 18140 } 18141 }, 18142 18143 /** 18144 * Check if user is logged in 18145 */ 18146 isUserLoggedIn() { 18147 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 18148 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 18149 }, 18150 18151 /** 18152 * Check if browser supports push notifications 18153 */ 18154 isSupported() { 18155 return ( 18156 'serviceWorker' in navigator && 18157 'PushManager' in window && 18158 'Notification' in window 18159 ); 18160 }, 18161 18162 /** 18163 * Load VAPID public key from server 18164 */ 18165 async loadVapidKey() { 18166 try { 18167 const response = await fetch('/api/chat/notifications/vapid_key', { 18168 method: 'GET', 18169 headers: { 18170 'Accept': 'application/json' 18171 } 18172 }); 18173 18174 if (!response.ok) { 18175 return false; 18176 } 18177 18178 const data = await response.json(); 18179 if (data.success && data.data?.public_key) { 18180 this.vapidPublicKey = data.data.public_key; 18181 return true; 18182 } 18183 18184 return false; 18185 18186 } catch (error) { 18187 console.error('[ChatNotification] Error loading VAPID key:', error); 18188 return false; 18189 } 18190 }, 18191 18192 /** 18193 * Register service worker 18194 */ 18195 async registerServiceWorker() { 18196 if (!('serviceWorker' in navigator)) return null; 18197 18198 try { 18199 const registration = await navigator.serviceWorker.register('/sw-push.js', { 18200 scope: '/' 18201 }); 18202 return registration; 18203 } catch (error) { 18204 return null; 18205 } 18206 }, 18207 18208 /** 18209 * Check subscription status from server 18210 */ 18211 async checkSubscriptionStatus() { 18212 try { 18213 const response = await fetch('/api/chat/notifications/status', { 18214 method: 'GET', 18215 credentials: 'include', 18216 headers: { 18217 'Accept': 'application/json' 18218 } 18219 }); 18220 18221 if (!response.ok) { 18222 return false; 18223 } 18224 18225 const data = await response.json(); 18226 return data.data?.subscribed || false; 18227 18228 } catch (error) { 18229 console.error('[ChatNotification] Error checking subscription:', error); 18230 return false;
18231 } 18232 }, 18233 18234 /** 18235 * Update UI based on status 18236 */ 18237 updateUI(status) { 18238 this.status = status; 18239 const btn = document.getElementById('chatNotifToggle'); 18240 18241 if (!btn) { 18242 // Retry after a short delay (button might not be in DOM yet) 18243 setTimeout(() => this.updateUI(status), 100); 18244 return; 18245 } 18246 18247 btn.setAttribute('data-status', status); 18248 18249 // Show/hide button based on support 18250 if (status === 'not-supported') { 18251 btn.style.display = 'none'; 18252 } else { 18253 btn.style.display = 'block'; 18254 } 18255 18256 // Update SVG icon (bell vs bell-slash) 18257 const svg = btn.querySelector('svg'); 18258 if (svg) { 18259 if (status === 'subscribed') { 18260 // Bell icon (active) 18261 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"/>'; 18262 } else { 18263 // Bell slash icon (inactive/blocked) 18264 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"/>'; 18265 } 18266 } 18267 18268 // Update tooltip 18269 const tooltips = { 18270 'not-subscribed': 'Klik untuk aktifkan notifikasi', 18271 'subscribed': 'Notifikasi aktif', 18272 'blocked': 'Notifikasi diblokir' 18273 }; 18274 18275 if (tooltips[status]) { 18276 btn.title = tooltips[status]; 18277 } 18278 18279 // Hide info bar if subscribed or blocked 18280 if (status === 'subscribed' || status === 'blocked') { 18281 this.hideInfoBar(); 18282 } 18283 }, 18284 18285 /** 18286 * Show info bar (if not dismissed) 18287 */ 18288 showInfoBar() { 18289 if (localStorage.getItem('chat_notif_info_dismissed')) { 18290 return; 18291 } 18292 18293 const infoBar = document.getElementById('chatNotifInfoBar'); 18294 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18295 18296 if (infoBar) { 18297 infoBar.style.display = 'flex'; 18298 18299 // Bind dismiss button here so it's always attached when bar is visible 18300 const btn = infoBar.querySelector('#chatNotifDismissBtn'); 18301 if (btn && !btn._dismissBound) { 18302 btn._dismissBound = true; 18303 btn.addEventListener('click', () => { 18304 infoBar.style.display = 'none'; 18305 localStorage.setItem('chat_notif_info_dismissed', '1'); 18306 if (popupBody) popupBody.classList.remove('has-info-bar'); 18307 }); 18308 } 18309 } 18310 18311 if (popupBody) { 18312 popupBody.classList.add('has-info-bar'); 18313 } 18314 }, 18315 18316 /** 18317 * Hide info bar 18318 */ 18319 hideInfoBar() { 18320 const infoBar = document.getElementById('chatNotifInfoBar'); 18321 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18322 18323 if (infoBar) { 18324 infoBar.style.display = 'none'; 18325 } 18326 18327 if (popupBody) { 18328 popupBody.classList.remove('has-info-bar'); 18329 } 18330 }, 18331 18332 /** 18333 * Dismiss info bar permanently 18334 */ 18335 dismissInfo() { 18336 this.hideInfoBar(); 18337 localStorage.setItem('chat_notif_info_dismissed', '1'); 18338 }, 18339 18340 /** 18341 * Handle bell icon click 18342 */ 18343 async handleToggleClick() { 18344 if (this.status === 'not-subscribed') { 18345 await this.subscribe(); 18346 } else if (this.status === 'subscribed') { 18347 await this.unsubscribe(); 18348 } else if (this.status === 'blocked') { 18349 this.showUnblockHelp(); 18350 } 18351 }, 18352 18353 /** 18354 * Subscribe to push notifications 18355 */ 18356 async subscribe() { 18357 try { 18358 // Request permission 18359 const permission = await Notification.requestPermission(); 18360 18361 if (permission === 'denied') { 18362 this.updateUI('blocked'); 18363 this.showToast('Notifikasi diblokir', 'error'); 18364 return; 18365 } 18366 18367 if (permission !== 'granted') { 18368 return; 18369 } 18370 18371 // Get service worker registration 18372 const registration = await navigator.serviceWorker.ready; 18373 18374 // Subscribe to push 18375 const subscription = await registration.pushManager.subscribe({ 18376 userVisibleOnly: true, 18377 applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) 18378 }); 18379 18380 // Send subscription to server 18381 const response = await fetch('/api/chat/notifications/subscribe', { 18382 method: 'POST', 18383 credentials: 'include', 18384 headers: { 18385 'Content-Type': 'application/json', 18386 'Accept': 'application/json' 18387 }, 18388 body: JSON.stringify(subscription.toJSON()) 18389 }); 18390 18391 if (!response.ok) { 18392 throw new Error('Failed to save subscription'); 18393 } 18394 18395 const result = await response.json(); 18396 18397 this.subscription = subscription; 18398 this.updateUI('subscribed'); 18399 this.hideInfoBar(); 18400 this.showToast('Notifikasi berhasil diaktifkan!', 'success'); 18401 18402 // Send test notification 18403 setTimeout(() => this.sendTestNotification(), 1000); 18404 18405 } catch (error) { 18406 console.error('[ChatNotification] Subscribe error:', error); 18407 this.showToast('Gagal mengaktifkan notifikasi', 'error'); 18408 } 18409 }, 18410 18411 /** 18412 * Unsubscribe from push notifications 18413 */ 18414 async unsubscribe() { 18415 try { 18416 // Confirm first 18417 if (!confirm('Nonaktifkan notifikasi chat?')) { 18418 return; 18419 } 18420 18421 // Get current subscription 18422 const registration = await navigator.serviceWorker.ready; 18423 const subscription = await registration.pushManager.getSubscription(); 18424 18425 if (subscription) { 18426 // Unsubscribe from browser 18427 await subscription.unsubscribe(); 18428 18429 // Tell server 18430 await fetch('/api/chat/notifications/unsubscribe', { 18431 method: 'POST', 18432 credentials: 'include', 18433 headers: { 18434 'Content-Type': 'application/json', 18435 'Accept': 'application/json' 18436 }, 18437 body: JSON.stringify(subscription.toJSON()) 18438 }); 18439 } 18440 18441 this.subscription = null; 18442 this.updateUI('not-subscribed'); 18443 this.showInfoBar(); 18444 this.showToast('Notifikasi dinonaktifkan', 'info'); 18445 18446 } catch (error) { 18447 console.error('[ChatNotification] Unsubscribe error:', error); 18448 this.showToast('Gagal menonaktifkan notifikasi', 'error'); 18449 } 18450 }, 18451 18452 /** 18453 * Send test notification 18454 */ 18455 async sendTestNotification() { 18456 try { 18457 const response = await fetch('/api/chat/notifications/send_test', { 18458 method: 'POST', 18459 credentials: 'include', 18460 headers: { 18461 'Accept': 'application/json' 18462 } 18463 }); 18464 18465 if (response.ok) { 18466 // Test notification sent successfully 18467 } 18468 } catch (error) { 18469 // Test notification failed silently 18470 } 18471 }, 18472 18473 /** 18474 * Show unblock help modal/message 18475 */ 18476 showUnblockHelp() { 18477 const message = ` 18478 Notifikasi diblokir oleh browser.\n\n 18479 Untuk mengaktifkan:\n 18480 1. Klik ikon gembok/info di address bar\n 18481 2. Cari "Notifications" atau "Notifikasi"\n 18482 3. Ubah menjadi "Allow" atau "Izinkan"\n 18483 4. Refresh halaman ini 18484 `.trim().replace(/\s+/g, ' '); 18485 18486 alert(message); 18487 }, 18488 18489 /** 18490 * Show toast notification 18491 */ 18492 showToast(message, type = 'info') { 18493 // Simple toast implementation 18494 const toast = document.createElement('div'); 18495 toast.className = `chat-toast chat-toast-${type}`; 18496 toast.textContent = message; 18497 toast.style.cssText = `
18498 position: fixed; 18499 bottom: 80px; 18500 right: 20px; 18501 background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; 18502 color: white; 18503 padding: 12px 20px; 18504 border-radius: 4px; 18505 box-shadow: 0 2px 8px rgba(0,0,0,0.2); 18506 z-index: 10000; 18507 animation: slideInUp 0.3s ease; 18508 `; 18509 18510 document.body.appendChild(toast); 18511 18512 setTimeout(() => { 18513 toast.style.animation = 'slideOutDown 0.3s ease'; 18514 setTimeout(() => toast.remove(), 300); 18515 }, 3000); 18516 }, 18517 18518 /** 18519 * Convert VAPID key to Uint8Array 18520 */ 18521 urlBase64ToUint8Array(base64String) { 18522 const padding = '='.repeat((4 - base64String.length % 4) % 4); 18523 const base64 = (base64String + padding) 18524 .replace(/\-/g, '+') 18525 .replace(/_/g, '/'); 18526 18527 const rawData = window.atob(base64); 18528 const outputArray = new Uint8Array(rawData.length); 18529 18530 for (let i = 0; i < rawData.length; ++i) { 18531 outputArray[i] = rawData.charCodeAt(i); 18532 } 18533 return outputArray; 18534 }, 18535 18536 /** 18537 * Check if push notification is currently active 18538 * @returns {Promise<boolean>} 18539 */ 18540 async isPushActive() { 18541 try { 18542 // Check browser subscription 18543 if (!('serviceWorker' in navigator)) return false; 18544 18545 const registration = await navigator.serviceWorker.getRegistration(); 18546 if (!registration) return false; 18547 18548 const subscription = await registration.pushManager.getSubscription(); 18549 if (!subscription) return false; 18550 18551 // Also check server status 18552 const serverStatus = await this.checkSubscriptionStatus(); 18553 18554 return serverStatus; 18555 } catch (error) { 18556 return false; 18557 } 18558 }, 18559 18560 /** 18561 * Show logout confirmation if push notification is active 18562 * Call this before logout action 18563 * @returns {Promise<boolean>} true if user confirms logout, false if cancelled 18564 */ 18565 async confirmLogoutWithPushNotification() { 18566 const isActive = await this.isPushActive(); 18567 18568 if (!isActive) { 18569 // No active push, proceed with logout 18570 return true; 18571 } 18572 18573 // Show modal confirmation using modal.css 18574 return new Promise((resolve) => { 18575 // Create modal HTML 18576 const modalHTML = ` 18577 <div class="modal-overlay" id="logoutConfirmModal"> 18578 <div class="modal-dialog"> 18579 <div class="modal-header"> 18580 <h3 class="modal-title"> 18581 <span class="modal-icon warning">â ï¸</span> 18582 Konfirmasi Logout 18583 </h3> 18584 </div> 18585 <div class="modal-body"> 18586 <p class="modal-message"> 18587 Anda memiliki <strong>notifikasi push yang aktif</strong>.<br><br> 18588 Setelah logout, Anda tidak akan menerima notifikasi chat lagi di perangkat ini.<br><br> 18589 Apakah Anda yakin ingin melanjutkan logout? 18590 </p> 18591 </div> 18592 <div class="modal-actions" style="padding: 0 20px 20px;"> 18593 <button class="modal-btn modal-btn-secondary" id="logoutCancelBtn"> 18594 Batal 18595 </button> 18596 <button class="modal-btn modal-btn-danger" id="logoutConfirmBtn"> 18597 Lanjutkan Logout 18598 </button> 18599 </div> 18600 </div> 18601 </div> 18602 `; 18603 18604 // Append to body 18605 const modalContainer = document.createElement('div'); 18606 modalContainer.innerHTML = modalHTML; 18607 document.body.appendChild(modalContainer.firstElementChild); 18608 18609 const modal = document.getElementById('logoutConfirmModal'); 18610 const confirmBtn = document.getElementById('logoutConfirmBtn'); 18611 const cancelBtn = document.getElementById('logoutCancelBtn'); 18612 18613 // Show modal with animation 18614 requestAnimationFrame(() => { 18615 modal.classList.add('show'); 18616 }); 18617 18618 // Handle confirm 18619 const handleConfirm = () => { 18620 cleanup(); 18621 resolve(true); 18622 }; 18623 18624 // Handle cancel 18625 const handleCancel = () => { 18626 cleanup(); 18627 resolve(false); 18628 }; 18629 18630 // Cleanup function 18631 const cleanup = () => { 18632 modal.classList.remove('show'); 18633 setTimeout(() => { 18634 modal.remove(); 18635 }, 200); 18636 confirmBtn.removeEventListener('click', handleConfirm); 18637 cancelBtn.removeEventListener('click', handleCancel); 18638 modal.removeEventListener('click', handleBackdropClick); 18639 }; 18640 18641 // Handle backdrop click 18642 const handleBackdropClick = (e) => { 18643 if (e.target === modal) { 18644 handleCancel(); 18645 } 18646 }; 18647 18648 // Add event listeners 18649 confirmBtn.addEventListener('click', handleConfirm); 18650 cancelBtn.addEventListener('click', handleCancel); 18651 modal.addEventListener('click', handleBackdropClick); 18652 }); 18653 } 18654}; 18655 18656// Add CSS animations 18657const chatNotifStyle = document.createElement('style'); 18658chatNotifStyle.textContent = ` 18659 @keyframes slideInUp { 18660 from { 18661 transform: translateY(100%); 18662 opacity: 0; 18663 } 18664 to { 18665 transform: translateY(0); 18666 opacity: 1; 18667 } 18668 } 18669 18670 @keyframes slideOutDown { 18671 from { 18672 transform: translateY(0); 18673 opacity: 1; 18674 } 18675 to { 18676 transform: translateY(100%); 18677 opacity: 0; 18678 } 18679 } 18680`; 18681document.head.appendChild(chatNotifStyle); 18682 18683// Auto-initialize when DOM is ready 18684if (document.readyState === 'loading') { 18685 document.addEventListener('DOMContentLoaded', () => ChatNotification.init()); 18686} else { 18687 ChatNotification.init(); 18688} 18689 18690// Expose to window for access from other scripts 18691window.ChatNotification = ChatNotification; 18692 18693</script>
18693 18694 <!-- Footer Collapse Toggle Script --> 18695
18695<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 18696 document.addEventListener('DOMContentLoaded', function() { 18697 const ftTop = document.getElementById('ftTop'); 18698 const expandBtn = document.getElementById('ftExpandBtn'); 18699 18700 if (ftTop && expandBtn) { 18701 const expandText = expandBtn.querySelector('.expand-text'); 18702 18703 expandBtn.addEventListener('click', function() { 18704 ftTop.classList.toggle('expanded'); 18705 const isExpanded = ftTop.classList.contains('expanded'); 18706 if (expandText) { 18707 expandText.textContent = isExpanded ? 'Sembunyikan' : 'Selengkapnya'; 18708 } 18709 }); 18710 } 18711 }); 18712 </script>
18712 18713 </footer> 18714 18715<!-- Compare Sidebar Overlay --> 18716<div class="compare-overlay" id="compareOverlay"></div> 18717 18718<!-- Floating Compare Badge (Sticky) --> 18719<button class="compare-floating-badge" id="compareFloatingBadge" aria-label="Lihat produk yang dibandingkan" style="display:none;"> 18720 <svg class="cfb-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 18721 <path d="M9.01 14H2v2h7.01v3L13 15l-3.99-4v3zm5.98-1v-3H22V8h-7.01V5L11 9l3.99 4z"/> 18722 </svg> 18723 <span class="cfb-text">Bandingkan</span> 18724 <span class="cfb-count" id="cfbCount">0</span> 18725</button> 18726 18727<!-- AI Comparison Modal --> 18728<div class="ai-compare-modal" id="aiCompareModal"> 18729 <div class="modal-backdrop"></div> 18730 <div class="modal-container"> 18731 <div class="modal-header"> 18732 <h3 class="modal-title"> 18733 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 18734 <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"/> 18735 </svg> 18736 Analisis AI: Perbandingan Produk 18737 </h3> 18738 <button class="modal-close" id="aiCompareModalClose" aria-label="Tutup"> 18739 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18740 <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"/> 18741 </svg> 18742 </button> 18743 </div> 18744 18745 <div class="modal-content"> 18746 <!-- Loading State --> 18747 <div class="ai-loading" id="aiLoading"> 18748 <div class="loading-animation"> 18749 <div class="typing-indicator"> 18750 <span></span> 18751 <span></span> 18752 <span></span> 18753 </div> 18754 <p>AI sedang menganalisis produk...</p> 18755 <small>Proses ini membutuhkan waktu 10-30 detik</small> 18756 </div> 18757 </div> 18758 18759 <!-- Error State --> 18760 <div class="ai-error" id="aiError" hidden> 18761 <div class="error-icon"> 18762 <svg width="48" height="48" viewBox="0 0 24 24" fill="#ef4444"> 18763 <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"/> 18764 </svg> 18765 </div> 18766 <h4>Analisis AI Gagal</h4> 18767 <p id="aiErrorMessage">Terjadi kesalahan saat memproses analisis.</p> 18768 <button class="btn btn-outline-primary" id="aiRetry">Coba Lagi</button> 18769 </div> 18770 18771 <!-- Analysis Results --> 18772 <div class="ai-results" id="aiResults" hidden> 18773 <!-- Summary Section --> 18774 <div class="ai-summary-card"> 18775 <h4> 18776 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18777 <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"/> 18778 </svg> 18779 Ringkasan Analisis 18780 </h4> 18781 <p id="aiSummary" class="summary-text"></p> 18782 <div class="winner-badge" id="winnerBadge"> 18783 <strong>Rekomendasi Terbaik:</strong> <span id="winnerProduct"></span> 18784 <small id="winnerReason"></small> 18785 </div> 18786 </div> 18787 18788 <!-- Comparison Table --> 18789 <div class="ai-comparison-table"> 18790 <h4> 18791 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18792 <path d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"/> 18793 </svg> 18794 Perbandingan Detail 18795 </h4> 18796 <div id="comparisonTable" class="comparison-grid"></div> 18797 </div> 18798 18799 <!-- Detailed Analysis --> 18800 <div class="ai-detailed-analysis"> 18801 <h4> 18802 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18803 <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"/> 18804 </svg> 18805 Analisis Mendalam 18806 </h4> 18807 <div id="detailedAnalysis" class="products-analysis"></div> 18808 </div> 18809 18810 <!-- Recommendations --> 18811 <div class="ai-recommendations"> 18812 <h4> 18813 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18814 <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"/> 18815 </svg> 18816 Rekomendasi Berdasarkan Kebutuhan 18817 </h4> 18818 <div class="recommendations-grid"> 18819 <div class="recommendation-card"> 18820 <h5>ð° Budget Conscious</h5> 18821 <p id="budgetRecommendation"></p> 18822 </div> 18823 <div class="recommendation-card"> 18824 <h5>â¡ Performance Focused</h5> 18825 <p id="performanceRecommendation"></p> 18826 </div> 18827 <div class="recommendation-card"> 18828 <h5>âï¸ Balanced Choice</h5> 18829 <p id="balancedRecommendation"></p> 18830 </div> 18831 </div> 18832 </div> 18833 18834 <!-- Buying Tips --> 18835 <div class="ai-tips"> 18836 <h4> 18837 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18838 <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
18838M5,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"/> 18839 </svg> 18840 Tips Pembelian 18841 </h4> 18842 <ul id="buyingTips" class="tips-list"></ul> 18843 </div> 18844 </div> 18845 </div> 18846 </div> 18847</div> 18848 18849<!-- Cart Login Modal --> 18850<div class="modal-overlay" id="cartLoginModal"> 18851 <div class="modal-dialog"> 18852 <div class="modal-header"> 18853 <h3 class="modal-title"> 18854 <div class="modal-icon info"> 18855 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18856 <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"/> 18857 </svg> 18858 </div> 18859 Login untuk Melanjutkan 18860 </h3> 18861 </div> 18862 <div class="modal-body"> 18863 <p class="modal-message" style="margin-bottom: 16px;">Silakan login atau daftar untuk mengakses keranjang belanja dan menyelesaikan pesanan Anda.</p> 18864 <div style="background: #f8fafc; border-radius: 8px; padding: 12px; margin-bottom: 20px;"> 18865 <p style="font-size: 14px; color: #646947; margin: 0; line-height: 1.6;"> 18866 <strong style="color: #2a2a0f;">Keuntungan dengan akun:</strong><br> 18867 â Simpan keranjang belanja Anda<br> 18868 â Lacak status pesanan secara real-time<br> 18869 â Checkout lebih cepat dengan data tersimpan<br> 18870 â Dapatkan notifikasi promo & diskon eksklusif 18871 </p> 18872 </div> 18873 <div class="modal-actions"> 18874 <button class="modal-btn modal-btn-secondary" id="cartLoginCancel">Batal</button> 18875 <button class="modal-btn modal-btn-primary" id="cartLoginConfirm" style="background: #ff6b35; border-color: #ff6b35;">Login</button> 18876 <button class="modal-btn modal-btn-primary" id="cartRegisterBtn" style="background: #f7931e; border-color: #f7931e;">Daftar</button> 18877 </div> 18878 </div> 18879 </div> 18880</div> 18881 18882
18883<script data-inlined="app.js"> 18884(function initLiveSearch(){ 18885 function setup() { 18886 const searchInput = document.getElementById('searchInput'); 18887 const suggest = document.getElementById('searchSuggest'); 18888 const searchBtn = document.getElementById('searchBtn'); 18889 const aiToggleBtn = document.getElementById('aiToggleBtn'); 18890 18891 if (!searchInput || !suggest || !aiToggleBtn) { 18892 console.log('[LiveSearch] Elemen belum ada, retry...'); 18893 return false; 18894 } 18895 18896 let debounceTimer = null; 18897 let lastQuery = ''; 18898 let currentIndex = -1; 18899 let currentAbortController = null; 18900 let isAIModeEnabled = false; // AI mode OFF by default 18901 let isAIProcessing = false; // Flag to prevent closing suggest box during AI processing 18902 18903 // Trending cache 18904 let trendingCache = null; 18905 let trendingLoading = false; 18906 18907 // Trending products cache 18908 let trendingProductsCache = null; 18909 let trendingProductsLoading = false; 18910 18911 const MIN_LEN = 2; 18912 const DEBOUNCE = 300; 18913 const AI_TIMEOUT = 15000; // 15 seconds timeout for AI endpoint 18914 18915 // Load AI mode preference from localStorage 18916 const savedAIMode = localStorage.getItem('plazait_ai_search_mode'); 18917 if (savedAIMode === 'enabled') { 18918 isAIModeEnabled = false; 18919 aiToggleBtn.classList.remove('active'); 18920 } 18921 18922 // Update placeholder based on AI mode 18923 function updatePlaceholder() { 18924 if (isAIModeEnabled) { 18925 searchInput.placeholder = 'ð¤ Coba: "laptop HP core i3 untuk kerja" atau "tinta epson l3210"'; 18926 } else { 18927 // Static placeholder for typing animation 18928 searchInput.placeholder = ''; 18929 } 18930 } 18931 18932 // Typing animation for placeholder 18933 let typingTimeout = null; 18934 let currentProductIndex = 0; 18935 let currentCharIndex = 0; 18936 let isTyping = false; 18937 let isDeleting = false; 18938 18939 const popularProducts = [ 18940 'Oppo A6', 18941 'Asus Ryzen 3', 18942 'Starlink V4', 18943 'HP Victus', 18944 'Lenovo RTX 5060', 18945 'ROG Strix', 18946 'iPhone 13', 18947 'Xpad 20', 18948 'Xiaomi 14', 18949 'Epson L3210', 18950 'Realme C85', 18951 'Brother T720', 18952 'Mouse Logitech' 18953 ]; 18954 18955 function typeWriter() { 18956 if (isAIModeEnabled || document.activeElement === searchInput) { 18957 return; // Skip typing if AI mode is on or input is focused 18958 } 18959 18960 const currentProduct = popularProducts[currentProductIndex]; 18961 18962 if (!isDeleting) { 18963 // Typing forward 18964 if (currentCharIndex < currentProduct.length) { 18965 searchInput.placeholder = currentProduct.substring(0, currentCharIndex + 1); 18966 currentCharIndex++; 18967 typingTimeout = setTimeout(typeWriter, 100); // Typing speed 18968 } else { 18969 // Finished typing, wait before deleting 18970 typingTimeout = setTimeout(() => { 18971 isDeleting = true; 18972 typeWriter(); 18973 }, 2000); // Pause before deleting 18974 } 18975 } else { 18976 // Deleting backward 18977 if (currentCharIndex > 0) { 18978 searchInput.placeholder = currentProduct.substring(0, currentCharIndex - 1); 18979 currentCharIndex--; 18980 typingTimeout = setTimeout(typeWriter, 50); // Deleting speed (faster) 18981 } else { 18982 // Finished deleting, move to next product 18983 isDeleting = false; 18984 currentProductIndex = (currentProductIndex + 1) % popularProducts.length; 18985 typingTimeout = setTimeout(typeWriter, 500); // Pause before typing next 18986 } 18987 } 18988 } 18989 18990 function startTypingAnimation() { 18991 if (!isAIModeEnabled && !typingTimeout) { 18992 currentCharIndex = 0; 18993 isDeleting = false; 18994 typeWriter(); 18995 } 18996 } 18997 18998 function stopTypingAnimation() { 18999 if (typingTimeout) { 19000 clearTimeout(typingTimeout); 19001 typingTimeout = null; 19002 } 19003 } 19004 19005 // Set initial placeholder 19006 updatePlaceholder(); 19007 19008 // Start typing animation for normal mode 19009 if (!isAIModeEnabled) { 19010 startTypingAnimation(); 19011 } 19012 19013 function esc(str){ 19014 if (str === null || str === undefined) return ''; 19015 return String(str) 19016 .replace(/&/g,'&') 19017 .replace(/</g,'<') 19018 .replace(/>/g,'>') 19019 .replace(/"/g,'"') 19020 .replace(/'/g,'''); 19021 } 19022 19023 function clearSuggest(){ 19024 suggest.innerHTML = ''; 19025 suggest.hidden = true; 19026 suggest.classList.remove('open', 'trending-mode'); 19027 currentIndex = -1; 19028 } 19029 19030 // ==================== TRENDING FUNCTIONS ==================== 19031 19032 async function fetchTrending() { 19033 if (trendingCache) return trendingCache; 19034 if (trendingLoading) return null; 19035 19036 trendingLoading = true; 19037
19038 try { 19039 const res = await fetch('/api/trending_searches.php?timeframe=7d&limit=8'); 19040 const data = await res.json(); 19041 19042 if (data.ok && Array.isArray(data.data)) { 19043 trendingCache = data.data; 19044 console.log('[Trending] Loaded', trendingCache.length, 'items'); 19045 return trendingCache; 19046 } 19047 } catch (err) { 19048 console.error('[Trending] Fetch error:', err); 19049 } finally { 19050 trendingLoading = false; 19051 } 19052 19053 return null; 19054 } 19055 19056 async function fetchTrendingProducts() { 19057 if (trendingProductsCache) return trendingProductsCache; 19058 if (trendingProductsLoading) return null; 19059 19060 trendingProductsLoading = true; 19061 19062 try { 19063 const res = await fetch('/api/trending_products.php?timeframe=7d&limit=6'); 19064 const data = await res.json(); 19065 19066 if (data.ok && Array.isArray(data.data)) { 19067 trendingProductsCache = data.data; 19068 console.log('[Trending Products] Loaded', trendingProductsCache.length, 'items'); 19069 return trendingProductsCache; 19070 } 19071 } catch (err) { 19072 console.error('[Trending Products] Fetch error:', err); 19073 } finally { 19074 trendingProductsLoading = false; 19075 } 19076 19077 return null; 19078 } 19079 19080 function renderTrendingSuggestions(trends, products) { 19081 if ((!trends || trends.length === 0) && (!products || products.length === 0)) { 19082 clearSuggest(); 19083 return; 19084 } 19085 19086 let html = `<div class="trending-container">`; 19087 19088 // Left column: Trending searches 19089 if (trends && trends.length > 0) { 19090 html += ` 19091 <div class="trending-column trending-searches"> 19092 <div class="trending-header"> 19093 <span class="trending-icon">ð¥</span> 19094 <span class="trending-title">Pencarian Trending</span> 19095 </div> 19096 `; 19097 19098 html += trends.map((t, idx) => { 19099 const categoryHint = t.category_hint ? `<span class="trending-category">${esc(t.category_hint)}</span>` : ''; 19100 return ` 19101 <div class="trending-item" data-query="${esc(t.query)}" data-index="${idx}"> 19102 <span class="trending-rank"> 19103 <img src="/assets/img/uptrend.image" alt="trending" width="16" height="16" style="vertical-align: middle;"> 19104 </span> 19105 <span class="trending-query">${esc(t.query)}</span> 19106 ${categoryHint} 19107 </div> 19108 `; 19109 }).join(''); 19110 19111 html += `</div>`; 19112 } 19113 19114 // Right column: Trending products 19115 if (products && products.length > 0) { 19116 html += ` 19117 <div class="trending-column trending-products"> 19118 <div class="trending-header"> 19119 <span class="trending-icon">â</span> 19120 <span class="trending-title">Produk Populer</span> 19121 </div> 19122 `; 19123 19124 html += products.map((p, idx) => { 19125 return ` 19126 <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}"> 19127 <img src="${esc(p.image)}" alt="${esc(p.name)}" class="trending-product-thumb"> 19128 <div class="trending-product-info"> 19129 <div class="trending-product-name">${esc(p.name)}</div> 19130 <div class="trending-product-price">${esc(p.price_label)}</div> 19131 </div> 19132 </div> 19133 `; 19134 }).join(''); 19135 19136 html += `</div>`; 19137 } 19138 19139 html += `</div>`; 19140 19141 suggest.innerHTML = html; 19142 suggest.hidden = false; 19143 suggest.classList.add('open', 'trending-mode'); 19144 suggest.style.display = 'block'; 19145 19146 console.log('[Trending] Rendered', trends?.length || 0, 'searches and', products?.length || 0, 'products'); 19147 } 19148 19149 function trackSearchClick(query, productId) { 19150 fetch('/api/track_search_click.php', { 19151 method: 'POST', 19152 headers: { 'Content-Type': 'application/json' }, 19153 body: JSON.stringify({ 19154 query: query, 19155 product_id: productId,
19156 click_type: 'suggestion' 19157 }) 19158 }).catch(() => {}); // silent fail 19159 } 19160 19161 function trackTrendingClick(query) { 19162 fetch('/api/track_search_click.php', { 19163 method: 'POST', 19164 headers: { 'Content-Type': 'application/json' }, 19165 body: JSON.stringify({ 19166 query: query, 19167 click_type: 'trending' 19168 }) 19169 }).catch(() => {}); 19170 } 19171 19172 function trackTrendingProductClick(productId, query) { 19173 fetch('/api/track_search_click.php', { 19174 method: 'POST', 19175 headers: { 'Content-Type': 'application/json' }, 19176 body: JSON.stringify({ 19177 query: query || 'trending_product', 19178 product_id: productId, 19179 click_type: 'trending_product' 19180 }) 19181 }).catch(() => {}); 19182 } 19183 19184 function trackSearchSubmit(query) { 19185 fetch('/api/track_search_click.php', { 19186 method: 'POST', 19187 headers: { 'Content-Type': 'application/json' }, 19188 body: JSON.stringify({ 19189 query: query, 19190 click_type: 'submit' 19191 }) 19192 }).catch(() => {}); 19193 } 19194 19195 function logFinalSearch(query) { 19196 // Log the final search to analytics (not during typing) 19197 fetch('/api/search_products.php?q=' + encodeURIComponent(query) + '&log=1', { 19198 headers: { 'Accept': 'application/json' } 19199 }).catch(() => {}); 19200 } 19201 19202 // ==================== END TRENDING FUNCTIONS ==================== 19203 19204 function clearSuggest(){ 19205 suggest.innerHTML = ''; 19206 suggest.hidden = true; 19207 suggest.classList.remove('open', 'trending-mode'); 19208 currentIndex = -1; 19209 } 19210 19211 // AI Toggle Handler 19212 aiToggleBtn.addEventListener('click', (e) => { 19213 e.preventDefault(); 19214 e.stopPropagation(); 19215 19216 isAIModeEnabled = !isAIModeEnabled; 19217 19218 if (isAIModeEnabled) { 19219 aiToggleBtn.classList.add('active'); 19220 localStorage.setItem('plazait_ai_search_mode', 'enabled'); 19221 updatePlaceholder(); // Update placeholder text 19222 stopTypingAnimation(); // Stop typing animation in AI mode 19223 19224 // Show AI prompt if there's a query 19225 const query = searchInput.value.trim(); 19226 if (query.length >= MIN_LEN) { 19227 showAISearchPrompt(query); 19228 } else { 19229 showAIActivationMessage(); 19230 } 19231 } else { 19232 aiToggleBtn.classList.remove('active'); 19233 localStorage.setItem('plazait_ai_search_mode', 'disabled'); 19234 updatePlaceholder(); // Update placeholder text 19235 startTypingAnimation(); // Start typing animation in normal mode 19236 clearSuggest(); 19237 19238 // Switch to normal search if there's a query 19239 const query = searchInput.value.trim(); 19240 if (query.length >= MIN_LEN) { 19241 performSearch(query); 19242 } 19243 } 19244 }); 19245 19246 function showAIActivationMessage() { 19247 suggest.innerHTML = ` 19248 <div class="ai-progress-container"> 19249 <div class="ai-progress-header"> 19250 <div class="ai-progress-icon">â¨</div> 19251 <div class="ai-progress-text"> 19252 <div class="ai-progress-title">AI Search Mode Activated!</div> 19253 <div class="ai-progress-subtitle">Ketik query Anda untuk pencarian cerdas</div> 19254 </div> 19255 </div> 19256 </div> 19257 `; 19258 suggest.hidden = false; 19259 suggest.classList.add('open'); 19260 suggest.style.display = 'block'; 19261 } 19262 19263 function renderAIInsights(insights){ 19264 if (!insights) return ''; 19265 19266 // Generate AI summary via backend endpoint 19267 const summary = insights.ai_summary || generateAISummary(insights); 19268 19269 if (!summary) return ''; 19270 19271 return ` 19272 <div class="ai-result-banner"> 19273 <div class="ai-result-icon">â¨</div> 19274 <div class="ai-result-text"> 19275 <div class="ai-result-title">AI Assistant</div> 19276 <div class="ai-result-description">${summary}</div> 19277 </div> 19278 </div> 19279 `; 19280 } 19281 19282 function generateAISummary(insights) { 19283 // Fallback client-side summary generation if backend doesn't provide ai_summary 19284 const productName = insights.product_name || ''; 19285 const categoryPath = insights.category_path || ''; 19286 const priceRange = insights.price_range || {}; 19287 const intent = insights.intent || ''; 19288 19289 let summary = ''; 19290 19291 // Parse category for natural display 19292 let categoryDisplay = ''; 19293 if (categoryPath) { 19294 const categoryParts = categoryPath.split('/');
19295 categoryDisplay = categoryParts[categoryParts.length - 1] || categoryParts[categoryParts.length - 2] || categoryPath; 19296 } 19297 19298 // Intent-based opening 19299 if (intent === 'cek_stok') { 19300 summary = `Berikut ketersediaan stok untuk <strong>${esc(productName)}</strong>. `; 19301 } else if (intent === 'rekomendasi_produk') { 19302 summary = `Berikut rekomendasi produk <strong>${esc(categoryDisplay || productName)}</strong> yang kami pilihkan untuk Anda. `; 19303 } else { 19304 // Default: cari_harga or general 19305 if (productName && categoryDisplay) { 19306 summary = `Kami menemukan <strong>${esc(productName)}</strong> di kategori ${esc(categoryDisplay)}`; 19307 } else if (productName) { 19308 summary = `Kami menemukan produk <strong>${esc(productName)}</strong>`; 19309 } else if (categoryDisplay) { 19310 summary = `Kami menemukan produk ${esc(categoryDisplay)}`; 19311 } 19312 19313 // Add price context 19314 if (priceRange.min > 0 && priceRange.max > 0) { 19315 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19316 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19317 summary += ` dengan kisaran harga ${minPrice} - ${maxPrice}`; 19318 } else if (priceRange.min > 0) { 19319 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19320 summary += ` dengan harga mulai ${minPrice}`; 19321 } else if (priceRange.max > 0) { 19322 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19323 summary += ` dengan harga maksimal ${maxPrice}`; 19324 } 19325 19326 summary += ' yang sesuai dengan kebutuhan Anda.'; 19327 } 19328 19329 return summary || esc(insights.explanation || ''); 19330 } 19331 19332 function showAIProgressSteps() { 19333 return ` 19334 <div class="ai-progress-container"> 19335 <div class="ai-progress-header"> 19336 <div class="ai-progress-icon">ð¤</div> 19337 <div class="ai-progress-text"> 19338 <div class="ai-progress-title">AI sedang menganalisis...</div> 19339 <div class="ai-progress-subtitle">Memproses query Anda dengan kecerdasan buatan</div> 19340 </div> 19341 </div> 19342 <div class="ai-progress-steps"> 19343 <div class="ai-progress-step active"> 19344 <div class="ai-step-icon">1</div> 19345 <div class="ai-step-text">Memahami maksud pencarian...</div> 19346 </div> 19347 <div class="ai-progress-step pending"> 19348 <div class="ai-step-icon">2</div> 19349 <div class="ai-step-text">Mengekstrak parameter pencarian...</div> 19350 </div> 19351 <div class="ai-progress-step pending"> 19352 <div class="ai-step-icon">3</div> 19353 <div class="ai-step-text">Mencari produk yang relevan...</div> 19354 </div> 19355 </div> 19356 </div> 19357 `; 19358 } 19359 19360 function updateAIProgressStep(step) { 19361 const steps = suggest.querySelectorAll('.ai-progress-step'); 19362 19363 steps.forEach((stepEl, idx) => { 19364 const icon = stepEl.querySelector('.ai-step-icon'); 19365 19366 if (idx < step) { 19367 stepEl.classList.remove('active', 'pending'); 19368 stepEl.classList.add('completed'); 19369 icon.textContent = 'â'; 19370 } else if (idx === step) { 19371 stepEl.classList.remove('pending', 'completed'); 19372 stepEl.classList.add('active'); 19373 } 19374 }); 19375 } 19376
19377 function renderSuggest(items, aiInsights){ 19378 if (!items.length){ 19379 clearSuggest(); 19380 return; 19381 } 19382 19383 let html = ''; 19384 19385 // Show AI insights if available 19386 if (aiInsights && aiInsights.explanation) { 19387 html += renderAIInsights(aiInsights); 19388 } 19389 19390 // Section header for products with hint 19391 html += ` 19392 <div class="suggest-section-header"> 19393 <div class="suggest-header-main"> 19394 <span class="suggest-header-icon">ð¦</span> 19395 <span class="suggest-header-text">Produk yang ditemukan (${items.length})</span> 19396 </div> 19397 <div class="suggest-header-hint">Tekan <kbd>Enter</kbd> untuk lihat semua hasil</div> 19398 </div> 19399 `; 19400 19401 // Render product items 19402 html += items.map((it, idx)=>{ 19403 // Normalisasi & fallback 19404 const id = it.id != null ? it.id : ''; 19405 const name = esc(it.name); 19406 const slug = esc(it.slug != null ? it.slug : it.id); 19407 const img = esc(it.image || '/assets/img/no-image.png'); 19408 const price = esc(it.price_label || ''); 19409 19410 const url = '/product/' + encodeURIComponent(slug); 19411 19412 return ` 19413 <div class="suggest-item" data-index="${idx}" data-url="${url}" data-id="${esc(id)}"> 19414 <img src="${img}" alt="${name}" class="suggest-thumb"> 19415 <div class="suggest-meta"> 19416 <div class="suggest-name">${name}</div> 19417 <div class="suggest-price">${price}</div> 19418 </div> 19419 </div> 19420 `; 19421 }).join(''); 19422 19423 // Footer hint for more results - only show if 10 items (means there might be more) 19424 if (items.length >= 10) { 19425 html += ` 19426 <div class="suggest-footer-hint"> 19427 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: -3px; margin-right: 6px;"> 19428 <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"/> 19429 </svg> 19430 <span>Lihat hasil lengkap dengan menekan <kbd>Enter</kbd> atau <a href="#" class="suggest-footer-link" id="footerSearchLink">klik di sini</a></span> 19431 </div> 19432 `; 19433 } 19434 19435 suggest.innerHTML = html; 19436 suggest.hidden = false; 19437 suggest.classList.add('open'); 19438 suggest.style.display = 'block'; 19439 suggest.style.visibility = 'visible'; 19440 suggest.style.opacity = '1'; 19441 } 19442 19443 function fetchRemote(q, shouldLog = false){ 19444 // Cancel previous request if any 19445 if (currentAbortController) { 19446 currentAbortController.abort(); 19447 } 19448 currentAbortController = new AbortController(); 19449 19450 // Always use standard search for normal mode 19451 // Only add log=1 parameter when shouldLog is true (intentional search) 19452 const logParam = shouldLog ? '&log=1' : ''; 19453 const url = '/api/search_products.php?q=' + encodeURIComponent(q) + logParam; 19454 const fetchOptions = { 19455 headers: {'Accept':'application/json'}, 19456 signal: currentAbortController.signal 19457 }; 19458 19459 return fetch(url, fetchOptions) 19460 .then(r=>r.json()) 19461 .then(j=>{ 19462 if (j.ok && Array.isArray(j.data)) { 19463 return j; 19464 } 19465 return { ok: false, data: [] }; 19466 }) 19467 .catch(err=>{ 19468 if (err.name === 'AbortError') { 19469 return { ok: false, data: [] }; 19470 } 19471 return { ok:false, data:[] }; 19472 }); 19473 } 19474 19475 function showNoResults(q, aiInsights) { 19476 suggest.innerHTML = ` 19477 <div style="padding: 32px 24px; text-align: center; color: #64748b;"> 19478 <div style="font-size: 56px; margin-bottom: 16px; opacity: 0.6;">ð</div> 19479 <div style="font-size: 16px; font-weight: 600; color: #334155; margin-bottom: 8px;"> 19480 Tidak ada hasil untuk "${esc(q)}" 19481 </div> 19482 ${aiInsights && aiInsights.explanation ? ` 19483 <div style="font-size: 13px; margin: 16px auto; padding: 12px 16px; background: #f1f5f9; border-radius: 8px; max-width: 400px; color: #475569;"> 19484 <strong style="color: #667eea;">ð¤ AI memahami:</strong><br> 19485 ${esc(aiInsights.explanation)} 19486 </div> 19487 ` : ''} 19488 <div style="font-size: 13px; margin-top: 16px; opacity: 0.8;"> 19489 Coba kata kunci lain atau hubungi kami untuk bantuan 19490 </div> 19491 </div> 19492 `; 19493 suggest.hidden = false;
19494 suggest.classList.add('open'); 19495 suggest.style.display = 'block'; 19496 } 19497 19498 function showErrorMessage() { 19499 suggest.innerHTML = ` 19500 <div style="padding: 32px 24px; text-align: center; color: #dcce77;"> 19501 <div style="font-size: 48px; margin-bottom: 12px;">â ï¸</div> 19502 <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;"> 19503 Pencarian tidak tersedia 19504 </div> 19505 <div style="font-size: 12px; opacity: 0.8;"> 19506 Silakan coba lagi dalam beberapa saat 19507 </div> 19508 </div> 19509 `; 19510 suggest.hidden = false; 19511 suggest.classList.add('open'); 19512 suggest.style.display = 'block'; 19513 } 19514 19515 function showAIErrorState(q) { 19516 suggest.innerHTML = ` 19517 <div class="ai-progress-container"> 19518 <div class="ai-progress-header"> 19519 <div class="ai-progress-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);">â¡</div> 19520 <div class="ai-progress-text"> 19521 <div class="ai-progress-title">AI sedang memproses...</div> 19522 <div class="ai-progress-subtitle">Menganalisis query: "${esc(q)}"</div> 19523 </div> 19524 </div> 19525 <div style="padding: 16px; margin-top: 12px; background: #fef3c7; border-radius: 8px; border-left: 3px solid #f59e0b;"> 19526 <div style="font-size: 13px; color: #92400e; line-height: 1.6;"> 19527 <strong style="display: block; margin-bottom: 4px;">ð¡ Sedang mencari produk terbaik untuk Anda</strong> 19528 AI kami sedang menganalisis database untuk menemukan hasil yang paling relevan. 19529 Mohon tunggu sebentar... 19530 </div> 19531 </div> 19532 <div style="padding: 12px 16px; margin-top: 12px; text-align: center;"> 19533 <button onclick="location.reload()" style="padding: 8px 16px; background: #fb923c; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;"> 19534 ð Refresh & Coba Lagi 19535 </button> 19536 </div> 19537 </div> 19538 `; 19539 suggest.hidden = false; 19540 suggest.classList.add('open'); 19541 suggest.style.display = 'block'; 19542 } 19543 19544 function triggerAISearch(q) { 19545 lastQuery = q; // Update lastQuery to prevent re-triggering 19546 isAIProcessing = true; // Set flag to prevent suggest box from closing 19547 19548 // Show AI progress UI - keep suggest box visible 19549 suggest.innerHTML = showAIProgressSteps(); 19550 suggest.hidden = false; 19551 suggest.classList.add('open'); 19552 suggest.style.display = 'block'; 19553 19554 setTimeout(() => updateAIProgressStep(1), 1000); 19555 setTimeout(() => updateAIProgressStep(2), 2500); 19556 19557 // Cancel previous request if any 19558 if (currentAbortController) { 19559 currentAbortController.abort(); 19560 } 19561 currentAbortController = new AbortController(); 19562 19563 const url = '/api/search_ai.php?q=' + encodeURIComponent(q); 19564 const fetchOptions = { 19565 headers: {'Accept':'application/json'}, 19566 signal: currentAbortController.signal 19567 }; 19568 19569 // Add timeout 19570 const timeoutId = setTimeout(() => { 19571 currentAbortController.abort(); 19572 19573 // Show fallback loading state 19574 suggest.innerHTML = ` 19575 <div class="ai-progress-container"> 19576 <div class="ai-progress-header"> 19577 <div class="ai-progress-icon">â³</div> 19578 <div class="ai-progress-text"> 19579 <div class="ai-progress-title">AI timeout - switching to standard search</div> 19580 <div class="ai-progress-subtitle">Mencari dengan metode tradisional...</div> 19581 </div> 19582 </div> 19583 </div> 19584 `; 19585 19586 // Fallback to traditional search 19587 fetch('/api/search_products.php?q=' + encodeURIComponent(q), { 19588 headers: { 'Accept': 'application/json' } 19589 }) 19590 .then(r => r.json()) 19591 .then(j => { 19592 isAIProcessing = false; // Reset flag 19593 if (j.ok && Array.isArray(j.data) && j.data.length) { 19594 renderSuggest(j.data, null); 19595 } else { 19596 showNoResults(q, null); 19597 } 19598 }) 19599 .catch(() => { 19600 isAIProcessing = false; // Reset flag 19601 showAIErrorState(q); 19602 }); 19603 }, AI_TIMEOUT); 19604 19605 fetch(url, fetchOptions) 19606 .then(r => r.json()) 19607 .then(j => { 19608 clearTimeout(timeoutId); 19609 isAIProcessing = false; // Reset flag when done 19610 19611 if (j.ok && Array.isArray(j.data)) { 19612 if (j.data.length > 0) { 19613 renderSuggest(j.data, j.ai_insights || null); 19614 } else { 19615 showNoResults(q, j.ai_insights || null); 19616 } 19617 } else { 19618 showAIErrorState(q); 19619 } 19620 }) 19621 .catch(err => { 19622 clearTimeout(timeoutId); 19623 isAIProcessing = false; // Reset flag on error 19624 if (err.name !== 'AbortError') { 19625 showAIErrorState(q); 19626 } 19627 }); 19628 } 19629 19630 function performSearch(q){ 19631 if (q.length < MIN_LEN){ 19632 clearSuggest(); 19633 return; 19634 } 19635 if (q === lastQuery) return; 19636 lastQuery = q; 19637 19638 // Normal mode only - AI mode uses triggerAISearch() 19639 fetchRemote(q).then(j=>{ 19640 if (j.ok && Array.isArray(j.data)){ 19641 if (j.data.length > 0) { 19642 renderSuggest(j.data, null); 19643 } else { 19644 showNoResults(q, null); 19645 } 19646 } else { 19647 clearSuggest(); 19648 } 19649 }).catch(() => { 19650 clearSuggest(); 19651 }); 19652 } 19653 19654 function debounce(){ 19655 const q = searchInput.value.trim(); 19656 clearTimeout(debounceTimer); 19657 19658 // If empty, show trending suggestions and products 19659 if (q.length === 0) { 19660 debounceTimer = setTimeout(async () => { 19661 const trends = trendingCache || await fetchTrending(); 19662 const products = trendingProductsCache || await fetchTrendingProducts(); 19663 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19664 renderTrendingSuggestions(trends, products); 19665 } else { 19666 clearSuggest(); 19667 } 19668 }, 100); 19669 return; 19670 } 19671 19672 // For AI mode, just show search prompt (no auto search) 19673 if (isAIModeEnabled && q.length >= MIN_LEN) { 19674 showAISearchPrompt(q); 19675 } else if (!isAIModeEnabled && q.length >= MIN_LEN) { 19676 // Normal mode: auto search with debounce 19677 debounceTimer = setTimeout(()=> performSearch(q), DEBOUNCE); 19678 } else { 19679 clearSuggest(); 19680 } 19681 } 19682 19683 function showAISearchPrompt(q) { 19684 suggest.innerHTML = ` 19685 <div class="ai-search-prompt"> 19686 <div class="ai-prompt-header"> 19687 <div class="ai-prompt-icon">ð¤</div> 19688 <div class="ai-prompt-text"> 19689 <div class="ai-prompt-title">Pencarian AI Siap!</div> 19690 <div class="ai-prompt-subtitle">Klik tombol di bawah untuk mencari dengan AI</div> 19691 </div> 19692 </div> 19693 <div class="ai-prompt-query"> 19694 <div class="ai-query-label">
19694Query Anda:</div> 19695 <div class="ai-query-text">"${esc(q)}"</div> 19696 </div> 19697 <button class="ai-search-trigger-btn" data-query="${esc(q)}"> 19698 <span class="ai-btn-icon">â¨</span> 19699 <span class="ai-btn-text">Cari dengan AI</span> 19700 </button> 19701 <div class="ai-prompt-footer"> 19702 AI akan menganalisis query Anda untuk hasil yang lebih relevan 19703 </div> 19704 </div> 19705 `; 19706 suggest.hidden = false; 19707 suggest.classList.add('open'); 19708 suggest.style.display = 'block'; 19709 } 19710 19711 function moveHighlight(dir){ 19712 if (suggest.hidden) return; 19713 const items = suggest.querySelectorAll('.suggest-item'); 19714 if (!items.length) return; 19715 currentIndex += dir; 19716 if (currentIndex < 0) currentIndex = items.length - 1; 19717 if (currentIndex >= items.length) currentIndex = 0; 19718 items.forEach(it=> it.classList.remove('active')); 19719 const active = items[currentIndex]; 19720 if (active){ 19721 active.classList.add('active'); 19722 active.scrollIntoView({block:'nearest'}); 19723 } 19724 } 19725 19726 function activate(){ 19727 const active = suggest.querySelector('.suggest-item.active'); 19728 if (!active) return; 19729 window.location.href = active.getAttribute('data-url'); 19730 } 19731 19732 searchInput.addEventListener('input', debounce); 19733 19734 // Stop typing animation when focused & load trending if empty 19735 searchInput.addEventListener('focus', async () => { 19736 stopTypingAnimation(); 19737 19738 const q = searchInput.value.trim(); 19739 19740 // If empty, show trending suggestions and products 19741 if (q.length === 0) { 19742 const trends = trendingCache || await fetchTrending(); 19743 const products = trendingProductsCache || await fetchTrendingProducts(); 19744 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19745 renderTrendingSuggestions(trends, products); 19746 } 19747 return; 19748 } 19749 19750 // If AI mode and has query, show AI prompt 19751 if (isAIModeEnabled && q.length >= MIN_LEN && suggest.hidden) { 19752 showAISearchPrompt(q); 19753 } 19754 }); 19755 19756 // Resume typing animation when blurred (if suggest is closed) 19757 searchInput.addEventListener('blur', () => { 19758 // Add delay to allow click events to fire first 19759 setTimeout(() => { 19760 if (!isAIModeEnabled && suggest.hidden && searchInput.value.trim() === '') { 19761 startTypingAnimation(); 19762 } 19763 }, 200); 19764 }); 19765 19766 searchInput.addEventListener('keydown', e=>{ 19767 switch(e.key){ 19768 case 'ArrowDown': moveHighlight(1); e.preventDefault(); break; 19769 case 'ArrowUp': moveHighlight(-1); e.preventDefault(); break; 19770 case 'Enter': 19771 if (!suggest.hidden){ 19772 activate(); 19773 e.preventDefault(); 19774 } else { 19775 // Direct enter without suggestion - track search submit and log final search 19776 const q = searchInput.value.trim(); 19777 if (q.length >= MIN_LEN) { 19778 trackSearchSubmit(q); 19779 logFinalSearch(q); 19780 } 19781 } 19782 break; 19783 case 'Escape': clearSuggest(); break; 19784 } 19785 }); 19786 19787 suggest.addEventListener('click', e=>{ 19788 // Check if footer link clicked 19789 const footerLink = e.target.closest('#footerSearchLink'); 19790 if (footerLink) { 19791 e.preventDefault(); 19792 const q = searchInput.value.trim(); 19793 if (q.length >= MIN_LEN) { 19794 trackSearchSubmit(q); 19795 logFinalSearch(q); 19796 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19797 } 19798 return; 19799 } 19800 19801 // Check if AI search button clicked 19802 const aiBtn = e.target.closest('.ai-search-trigger-btn'); 19803 if (aiBtn) { 19804 const query = aiBtn.getAttribute('data-query'); 19805 if (query) { 19806 triggerAISearch(query); 19807 } 19808 return; 19809 } 19810 19811 // Handle trending item click 19812 const trendingItem = e.target.closest('.trending-item'); 19813 if (trendingItem) { 19814 const query = trendingItem.getAttribute('data-query'); 19815 if (query) { 19816 searchInput.value = query; 19817 trackTrendingClick(query); 19818 logFinalSearch(query); // Log final search when user clicks trending 19819 performSearch(query); 19820 } 19821 return; 19822 } 19823 19824 // Handle trending product click 19825 const trendingProduct = e.target.closest('.trending-product-item'); 19826 if (trendingProduct) { 19827 const productId = trendingProduct.getAttribute('data-product-id'); 19828 const slug = trendingProduct.getAttribute('data-slug'); 19829 const query = trendingProduct.getAttribute('data-query'); 19830 19831 if (productId && slug) {
19832 trackTrendingProductClick(productId, query); 19833 window.location.href = '/product/' + encodeURIComponent(slug); 19834 } 19835 return; 19836 } 19837 19838 // Handle product item click 19839 const it = e.target.closest('.suggest-item'); 19840 if (!it) return; 19841 19842 const productId = it.getAttribute('data-id'); 19843 trackSearchClick(lastQuery, productId); 19844 logFinalSearch(lastQuery); // Log final search when user clicks on suggestion 19845 19846 window.location.href = it.getAttribute('data-url'); 19847 }); 19848 19849 document.addEventListener('click', e=>{ 19850 // Don't close suggest box if AI is processing 19851 if (isAIProcessing) return; 19852 19853 if (e.target === searchInput || e.target === aiToggleBtn || 19854 suggest.contains(e.target) || aiToggleBtn.contains(e.target)) return; 19855 clearSuggest(); 19856 }); 19857 19858 if (searchBtn){ 19859 searchBtn.addEventListener('click', ()=>{ 19860 const q = searchInput.value.trim(); 19861 if (q.length >= MIN_LEN){ 19862 // Track search submit and log final search 19863 trackSearchSubmit(q); 19864 logFinalSearch(q); 19865 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19866 } 19867 }); 19868 } 19869 19870 // Pastikan style dasar muncul 19871 if (!suggest.classList.contains('wired')) { 19872 suggest.classList.add('wired'); 19873 suggest.style.position = 'absolute'; 19874 suggest.style.zIndex = '9999'; 19875 suggest.style.background = '#fff'; 19876 suggest.style.border = '1px solid #ccc'; 19877 suggest.style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)'; 19878 suggest.style.maxHeight = '360px'; 19879 suggest.style.overflowY = 'auto'; 19880 } 19881 19882 return true; 19883 } 19884 19885 if (!setup()) { 19886 if (document.readyState === 'loading') { 19887 document.addEventListener('DOMContentLoaded', setup); 19888 } else { 19889 setTimeout(setup, 80); 19890 } 19891 } 19892})(); 19893</script>
vendor: 1 bytes, line 19893
19893
19894<script data-inlined="site-notice.js"> 19895/** 19896 * Site Notice Banner - Interactive Behavior 19897 * Handles dismiss functionality with localStorage persistence 19898 * Supports dynamic notice IDs for different campaigns 19899 */ 19900 19901(function() { 19902 'use strict'; 19903 19904 const STORAGE_KEY = 'plazait_site_notice_dismissed'; 19905 19906 const banner = document.getElementById('holidayNotice'); 19907 const closeBtn = document.getElementById('holidayNoticeClose'); 19908 19909 if (!banner || !closeBtn) return; 19910 19911 // Get notice ID from data attribute 19912 const noticeId = banner.dataset.noticeId; 19913 if (!noticeId) { 19914 console.warn('Notice ID not found. Banner will not persist dismiss state.'); 19915 } 19916 19917 /** 19918 * Check if banner was previously dismissed 19919 */ 19920 function wasDismissed() { 19921 if (!noticeId) return false; 19922 19923 try { 19924 const dismissed = localStorage.getItem(STORAGE_KEY); 19925 return dismissed === noticeId; 19926 } catch (e) { 19927 console.warn('localStorage not available:', e); 19928 return false; 19929 } 19930 } 19931 19932 /** 19933 * Mark banner as dismissed 19934 */ 19935 function markDismissed() { 19936 if (!noticeId) return; 19937 19938 try { 19939 localStorage.setItem(STORAGE_KEY, noticeId); 19940 } catch (e) { 19941 console.warn('Could not save to localStorage:', e); 19942 } 19943 } 19944 19945 /** 19946 * Hide banner with animation 19947 */ 19948 function hideBanner() { 19949 banner.classList.add('hidden'); 19950 document.body.classList.remove('has-holiday-notice'); 19951 19952 // Remove from DOM after animation 19953 setTimeout(() => { 19954 banner.remove(); 19955 }, 300); 19956 19957 markDismissed(); 19958 19959 // Analytics tracking (optional) 19960 if (window.gtag) { 19961 window.gtag('event', 'banner_dismiss', { 19962 'event_category': 'Site Notice', 19963 'event_label': noticeId || 'unknown', 19964 }); 19965 } 19966 } 19967 19968 /** 19969 * Initialize banner 19970 */ 19971 function init() { 19972 // Check if already dismissed 19973 if (wasDismissed()) { 19974 banner.remove(); 19975 return; 19976 } 19977 19978 // Add body class for layout adjustment 19979 document.body.classList.add('has-holiday-notice'); 19980 19981 // Close button click 19982 closeBtn.addEventListener('click', hideBanner); 19983 19984 // Keyboard support for close button 19985 closeBtn.addEventListener('keydown', (e) => { 19986 if (e.key === 'Enter' || e.key === ' ') { 19987 e.preventDefault(); 19988 hideBanner(); 19989 } 19990 }); 19991 19992 // Optional: Auto-hide after certain time (disabled by default) 19993 // setTimeout(hideBanner, 30000); // 30 seconds 19994 } 19995 19996 // Initialize when DOM is ready 19997 if (document.readyState === 'loading') { 19998 document.addEventListener('DOMContentLoaded', init); 19999 } else { 20000 init(); 20001 } 20002 20003})(); 20004 20005</script>
vendor: 1 bytes, line 20005
20005
20006<script data-inlined="guest-cart.js"> 20007/** 20008 * Guest Cart Manager - localStorage based cart for guest users 20009 * 20010 * Manages shopping cart items in browser localStorage for users who are not logged in. 20011 * Provides seamless transition to database when user logs in. 20012 */ 20013 20014class GuestCart { 20015 constructor() { 20016 this.storageKey = 'plazait_guest_cart'; 20017 this.apiBase = '/api/cart'; 20018 20019 // Initialize cart structure 20020 this.cart = this.loadFromStorage(); 20021 20022 // Bind methods 20023 this.add = this.add.bind(this); 20024 this.remove = this.remove.bind(this); 20025 this.update = this.update.bind(this); 20026 this.clear = this.clear.bind(this); 20027 this.getItems = this.getItems.bind(this); 20028 this.mergeToDatabase = this.mergeToDatabase.bind(this); 20029 } 20030 20031 /** 20032 * Load cart from localStorage 20033 */ 20034 loadFromStorage() { 20035 try { 20036 const stored = localStorage.getItem(this.storageKey); 20037 if (stored) { 20038 const parsed = JSON.parse(stored); 20039 if (parsed && typeof parsed === 'object' && parsed.items) { 20040 return parsed; 20041 } 20042 } 20043 } catch (error) { 20044 // Silent error handling 20045 } 20046 20047 return { 20048 items: {}, 20049 updated_at: Date.now(), 20050 version: '1.0' 20051 }; 20052 } 20053 20054 /** 20055 * Save cart to localStorage 20056 */ 20057 saveToStorage() { 20058 try { 20059 this.cart.updated_at = Date.now(); 20060 localStorage.setItem(this.storageKey, JSON.stringify(this.cart)); 20061 20062 // Dispatch custom event for cart updates 20063 window.dispatchEvent(new CustomEvent('guestCartUpdated', { 20064 detail: { 20065 itemsCount: this.getItemsCount(), 20066 linesCount: this.getLinesCount(), 20067 items: this.cart.items 20068 } 20069 })); 20070 20071 return true; 20072 } catch (error) { 20073 return false; 20074 } 20075 } 20076 20077 /** 20078 * Generate cart item key 20079 */ 20080 generateKey(productId, variantId = 0) { 20081 return `p${productId}v${variantId}`; 20082 } 20083 20084 /** 20085 * Add item to guest cart 20086 */ 20087 add(productId, qty = 1, variantId = 0, meta = {}) { 20088 const key = this.generateKey(productId, variantId); 20089 20090 // Validate inputs 20091 if (!productId || productId <= 0 || !qty || qty <= 0) { 20092 throw new Error('Invalid product ID or quantity'); 20093 } 20094 20095 qty = Math.max(1, Math.min(999, parseInt(qty))); 20096 20097 // Initialize item if not exists 20098 if (!this.cart.items[key]) { 20099 this.cart.items[key] = { 20100 pid: parseInt(productId), 20101 vid: parseInt(variantId) || 0, 20102 qty: 0, 20103 name: meta.name || '', 20104 sku: meta.sku || '', 20105 price: parseInt(meta.price) || 0, 20106 image_url: meta.image_url || '', 20107 product_url: meta.product_url || '/product/' + parseInt(productId), 20108 added_at: Date.now() 20109 }; 20110 } 20111 20112 // Update quantity 20113 this.cart.items[key].qty = Math.max(1, Math.min(999, this.cart.items[key].qty + qty)); 20114 20115 // Update metadata if provided 20116 if (meta.name) this.cart.items[key].name = meta.name; 20117 if (meta.sku) this.cart.items[key].sku = meta.sku; 20118 if (meta.price) this.cart.items[key].price = parseInt(meta.price); 20119 if (meta.image_url) this.cart.items[key].image_url = meta.image_url; 20120 if (meta.product_url) this.cart.items[key].product_url = meta.product_url; 20121 20122 this.saveToStorage(); 20123 20124 return { 20125 key, 20126 item: this.cart.items[key], 20127 itemsCount: this.getItemsCount(), 20128 linesCount: this.getLinesCount() 20129 }; 20130 } 20131 20132 /** 20133 * Remove item from guest cart 20134 */ 20135 remove(productId, variantId = 0) { 20136 const key = this.generateKey(productId, variantId); 20137 20138 if (this.cart.items[key]) { 20139 delete this.cart.items[key]; 20140 this.saveToStorage(); 20141 20142 return true; 20143 } 20144 20145 return false;
20146 } 20147 20148 /** 20149 * Update item quantity 20150 */ 20151 update(productId, qty, variantId = 0) { 20152 const key = this.generateKey(productId, variantId); 20153 20154 if (!this.cart.items[key]) { 20155 return false; 20156 } 20157 20158 qty = parseInt(qty); 20159 if (qty <= 0) { 20160 return this.remove(productId, variantId); 20161 } 20162 20163 this.cart.items[key].qty = Math.max(1, Math.min(999, qty)); 20164 this.saveToStorage(); 20165 20166 return true; 20167 } 20168 20169 /** 20170 * Clear entire cart 20171 */ 20172 clear() { 20173 this.cart = { 20174 items: {}, 20175 updated_at: Date.now(), 20176 version: '1.0' 20177 }; 20178 20179 this.saveToStorage(); 20180 20181 return true; 20182 } 20183 20184 /** 20185 * Get all cart items 20186 */ 20187 getItems() { 20188 return this.cart.items; 20189 } 20190 20191 /** 20192 * Get cart items as array 20193 */ 20194 getItemsArray() { 20195 return Object.values(this.cart.items); 20196 } 20197 20198 /** 20199 * Get total items count (sum of quantities) 20200 */ 20201 getItemsCount() { 20202 return Object.values(this.cart.items).reduce((total, item) => total + (item.qty || 0), 0); 20203 } 20204 20205 /** 20206 * Get lines count (number of different items) 20207 */ 20208 getLinesCount() { 20209 return Object.keys(this.cart.items).length; 20210 } 20211 20212 /** 20213 * Get cart summary 20214 */ 20215 getSummary() { 20216 const itemsCount = this.getItemsCount(); 20217 const linesCount = this.getLinesCount(); 20218 const totalValue = Object.values(this.cart.items).reduce( 20219 (total, item) => total + ((item.qty || 0) * (item.price || 0)), 0 20220 ); 20221 20222 return { 20223 itemsCount, 20224 linesCount, 20225 totalValue, 20226 updatedAt: this.cart.updated_at 20227 }; 20228 } 20229 20230 /** 20231 * Check if cart has items 20232 */ 20233 isEmpty() { 20234 return this.getLinesCount() === 0; 20235 } 20236 20237 /** 20238 * Merge guest cart to database after user login 20239 */ 20240 async mergeToDatabase() { 20241 const items = this.cart.items; 20242 20243 if (Object.keys(items).length === 0) { 20244 return { ok: true, merged_count: 0, message: 'No items to merge' }; 20245 } 20246 20247 try { 20248 // Use new endpoint that handles localStorage format 20249 const response = await fetch(`${this.apiBase}/merge-localStorage.php`, { 20250 method: 'POST', 20251 headers: { 20252 'Content-Type': 'application/json', 20253 }, 20254 credentials: 'same-origin', 20255 body: JSON.stringify({ 20256 items: items // Send localStorage format directly 20257 }) 20258 }); 20259 20260 const result = await response.json(); 20261 20262 if (result.ok) { 20263 // Clear localStorage cart after successful merge 20264 this.clear(); 20265 20266 // Update cart counter 20267 if (window.updateCartCounter) { 20268 try { 20269 await window.updateCartCounter(); 20270 } catch (e) { 20271 // Silent error handling 20272 } 20273 } 20274 20275 // Dispatch merge completion event 20276 window.dispatchEvent(new CustomEvent('guestCartMerged', { 20277 detail: result 20278 })); 20279 20280 return result; 20281 } else { 20282 throw new Error(result.error || 'Failed to merge cart'); 20283 } 20284 20285 } catch (error) { 20286 throw error; 20287 } 20288 } 20289 20290 /** 20291 * Export cart data (for debugging or backup) 20292 */ 20293 export() { 20294 return { 20295 ...this.cart, 20296 summary: this.getSummary() 20297 }; 20298 } 20299 20300 /** 20301 * Import cart data (for restore or testing) 20302 */ 20303 import(cartData) { 20304 if (cartData && typeof cartData === 'object' && cartData.items) { 20305 this.cart = { 20306 items: cartData.items, 20307 updated_at: cartData.updated_at || Date.now(), 20308 version: cartData.version || '1.0' 20309 }; 20310 20311 this.saveToStorage(); 20312 return true; 20313 } 20314 20315 return false;
20316 } 20317} 20318 20319// Create global instance 20320window.guestCart = new GuestCart(); 20321 20322// Auto-merge on page load if user is logged in 20323document.addEventListener('DOMContentLoaded', async function() { 20324 // Multiple ways to check if user is logged in 20325 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20326 document.body.classList.contains('logged-in') || 20327 window.userLoggedIn === true || 20328 document.body.dataset.userLoggedIn === 'true'; 20329 20330 if (isLoggedIn && !window.guestCart.isEmpty()) { 20331 // Small delay to ensure page is fully loaded 20332 setTimeout(async () => { 20333 try { 20334 const result = await window.guestCart.mergeToDatabase(); 20335 } catch (error) { 20336 // Silent error handling 20337 } 20338 }, 500); 20339 } 20340}); 20341 20342// Also listen for login events (if login happens via AJAX) 20343window.addEventListener('userLoggedIn', async function() { 20344 if (!window.guestCart.isEmpty()) { 20345 try { 20346 await window.guestCart.mergeToDatabase(); 20347 } catch (error) { 20348 // Silent error handling 20349 } 20350 } 20351}); 20352</script>
vendor: 1 bytes, line 20352
20352
20353<script data-inlined="cart.js"> 20354(function(){ 20355 function $(s,ctx=document){ return ctx.querySelector(s); } 20356 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 20357 20358 // CSRF token helper 20359 function getCsrfToken() { 20360 const metaTag = document.querySelector('meta[name="csrf-token"]'); 20361 return metaTag ? metaTag.getAttribute('content') : ''; 20362 } 20363 20364 function guessPdMeta(){ 20365 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 20366 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 20367 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 20368 const imgEl = $('#pd-main-img'); 20369 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 20370 return { name, sku, image_url }; // price removed for security 20371 } 20372 20373 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 20374 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 20375 const payload = { 20376 product_id: parseInt(productId||0,10) || 0, 20377 qty: parseInt(qty||1,10) || 1, 20378 variant_id: parseInt(variantId||0,10) || 0, 20379 meta 20380 }; 20381 20382 // Check if user is logged in 20383 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20384 document.body.classList.contains('logged-in') || 20385 window.userLoggedIn === true || 20386 document.body.dataset.userLoggedIn === 'true'; 20387 20388 // Skip localStorage untuk logged user - mereka pakai database 20389 if (!isLoggedIn && window.guestCart) { 20390 try { 20391 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 20392 } catch (e) { 20393 console.warn('Failed to add to localStorage cart:', e); 20394 } 20395 } 20396 20397 const res = await fetch('/api/cart/add.php', { 20398 method: 'POST', 20399 headers: { 20400 'Content-Type':'application/json', 20401 'X-CSRF-Token': getCsrfToken() 20402 }, 20403 credentials: 'same-origin', 20404 body: JSON.stringify({ 20405 ...payload, 20406 csrf_token: getCsrfToken() 20407 }), 20408 }); 20409 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 20410 if (!res.ok || !data.ok) { 20411 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 20412 throw new Error(msg); 20413 } 20414 20415 // Update badge di navbar dengan data dari response 20416 try { 20417 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20418 if (el && data.cart && typeof data.cart.items_count === 'number') { 20419 el.textContent = String(data.cart.items_count); 20420 el.classList.toggle('is-empty', data.cart.items_count <= 0); 20421 } 20422 } catch(_){} 20423 20424 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 20425 try { 20426 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 20427 } catch(_){} 20428 20429 // Update CartBadge dengan data dari response (lebih reliable) 20430 try { 20431 if (window.CartBadge && window.CartBadge.set) { 20432 window.CartBadge.set(data.cart.items_count||0); 20433 } 20434 } catch(_){} 20435 20436 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 20437 setTimeout(() => { 20438 if (window.updateCartCounter) { 20439 window.updateCartCounter(); 20440 } 20441 }, 100); 20442 20443 return data; 20444 } 20445 20446 // Function to update cart counter from server 20447 async function updateCartCounter(){ 20448 try { 20449 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 20450 const data = await res.json().catch(()=>({ok:false})); 20451 if (data.ok && typeof data.items_count === 'number') { 20452 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20453 if (el) { 20454 el.textContent = String(data.items_count); 20455 el.classList.toggle('is-empty', data.items_count <= 0); 20456 } 20457 // Dispatch event 20458 try { 20459 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 20460 } catch(_){} 20461 } 20462 } catch (err) { 20463 console.log('Could not update cart counter:', err); 20464 } 20465 } 20466 20467 // Function to manually merge guest cart (if needed) 20468 async function mergeGuestCart(){ 20469 try { 20470 const res = await fetch('/api/cart/merge-guest.php', { 20471 method: 'POST', 20472 headers: { 20473 'Content-Type': 'application/json', 20474 'X-CSRF-Token': getCsrfToken() 20475 }, 20476 credentials: 'same-origin', 20477 body: JSON.stringify({ csrf_token: getCsrfToken() }) 20478 }); 20479 const data = await res.json().catch(()=>({ok:false})); 20480 20481 if (data.ok && data.merged_items > 0) { 20482 console.log(`Guest cart merged: ${data.merged_items} items`); 20483 // Update cart counter after merge 20484 await updateCartCounter(); 20485 return data; 20486 } 20487 return data; 20488 } catch (err) { 20489 console.log('Could not merge guest cart:', err); 20490 return { ok: false, error: err.message }; 20491 } 20492 } 20493 20494 window.addToCart = addToCart; 20495 window.updateCartCounter = updateCartCounter; 20496 window.mergeGuestCart = mergeGuestCart; 20497 20498 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 20499 document.addEventListener('click', async (e)=>{ 20500 const btn = e.target.closest('[data-addtocart]'); 20501 if (!btn) return; 20502 e.preventDefault(); 20503 if (btn.disabled) return; 20504 20505 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 20506 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
20507 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 20508 20509 try { 20510 const data = await addToCart(pid, qty, vid); 20511 if (window.showToast) { 20512 const meta = guessPdMeta(); 20513 window.showToast({ 20514 type:'success', 20515 title:'Berhasil ditambahkan', 20516 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 20517 duration: 3800, 20518 actions: [ 20519 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 20520 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 20521 ] 20522 }); 20523 } 20524 } catch (err) { 20525 if (window.showToast) { 20526 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 20527 } else { 20528 console.error(err); 20529 } 20530 } 20531 }); 20532})(); 20533</script>
vendor: 1 bytes, line 20533
20533
20534<script data-inlined="mega-menu-tiles.js"> 20535// Tile Mega Menu â Enhanced 2025 (Stagger animations, product counts, loading states) 20536// Includes safe-area hover handling (no backdrop flicker) 20537 20538(function(){ 20539 const trigger = document.querySelector('[data-mm2="trigger"]'); 20540 const panel = document.querySelector('[data-mm2="panel"]'); 20541 const topView = document.querySelector('[data-mm2="top"]'); 20542 const detailView= document.querySelector('[data-mm2="detail"]'); 20543 const backdrop = document.querySelector('[data-mm2="backdrop"]'); 20544 if (!trigger || !panel || !topView || !detailView) return; 20545 20546 const apiUrl = '/api/categories_tree.php?top=16&child=18&grand=14'; 20547 const isDesktop = () => window.matchMedia('(min-width: 961px)').matches; 20548 20549 let data = []; 20550 let hoverTimer, closeTimer; 20551 let isLoading = false; 20552 20553 const esc = s => (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 20554 20555 // Mobile close button HTML 20556 const mobileCloseBtn = () => ` 20557 <button type="button" class="mm2-mobile-close" data-mm2="close" aria-label="Tutup menu"> 20558 <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"> 20559 <line x1="18" y1="6" x2="6" y2="18"></line> 20560 <line x1="6" y1="6" x2="18" y2="18"></line> 20561 </svg> 20562 </button> 20563 `; 20564 20565 // Category-specific gradient colors (16 unique combinations) 20566 const categoryGradients = [ 20567 { from: '#e0e7ff', to: '#fff', border: '#c7d2fe', color: '#4f46e5' }, // Indigo 20568 { from: '#dbeafe', to: '#fff', border: '#bfdbfe', color: '#1e40af' }, // Blue 20569 { from: '#d1fae5', to: '#fff', border: '#a7f3d0', color: '#059669' }, // Green 20570 { from: '#fef3c7', to: '#fff', border: '#fde68a', color: '#d97706' }, // Amber 20571 { from: '#fce7f3', to: '#fff', border: '#fbcfe8', color: '#db2777' }, // Pink 20572 { from: '#e9d5ff', to: '#fff', border: '#d8b4fe', color: '#9333ea' }, // Purple 20573 { from: '#ffedd5', to: '#fff', border: '#fed7aa', color: '#ea580c' }, // Orange 20574 { from: '#ccfbf1', to: '#fff', border: '#99f6e4', color: '#0d9488' }, // Teal 20575 { from: '#fef9c3', to: '#fff', border: '#fef08a', color: '#ca8a04' }, // Yellow 20576 { from: '#ffe4e6', to: '#fff', border: '#fecdd3', color: '#e11d48' }, // Rose 20577 { from: '#e0f2fe', to: '#fff', border: '#bae6fd', color: '#0284c7' }, // Sky 20578 { from: '#f3e8ff', to: '#fff', border: '#e9d5ff', color: '#a855f7' }, // Violet 20579 { from: '#dcfce7', to: '#fff', border: '#bbf7d0', color: '#16a34a' }, // Emerald 20580 { from: '#fef2f2', to: '#fff', border: '#fecaca', color: '#dcce77' }, // Red 20581 { from: '#eff6ff', to: '#fff', border: '#dbeafe', color: '#2563eb' }, // Blue-600 20582 { from: '#f5f3ff', to: '#fff', border: '#ede9fe', color: '#7c3aed' }, // Violet-600 20583 ]; 20584 20585 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>`; 20586 20587 // Recent (optional future use) 20588 const REC_KEY='mm2_recent'; 20589 const saveRecent=(path,label)=>{ try{ 20590 let list=JSON.parse(localStorage.getItem(REC_KEY)||'[]').filter(x=>x&&x.path!==path); 20591 list.unshift({path,label,t:Date.now()}); localStorage.setItem(REC_KEY, JSON.stringify(list.slice(0,8))); 20592 }catch{} }; 20593 20594 function isToSafeArea(e){ 20595 const t = e && e.relatedTarget ? e.relatedTarget : null; 20596 if (!t) return false; 20597 return (trigger.contains(t) || panel.contains(t) || (backdrop && backdrop.contains(t))); 20598 } 20599 20600 function open(){ 20601 document.body.classList.add('mm2-open'); 20602 showTop(); 20603 // Add entrance animation class 20604 setTimeout(() => panel.classList.add('mm2-animated'), 10); 20605 } 20606 function close(){ 20607 document.body.classList.remove('mm2-open'); 20608 panel.classList.remove('mm2-animated'); 20609 } 20610 function showTop(){ 20611 topView.classList.add('is-active'); 20612 detailView.classList.remove('is-active'); 20613 } 20614 20615 // Loading skeleton 20616 function showLoading() { 20617 const skeletons = Array.from({length: 12}, (_, i) => ` 20618 <div class="mm2-card mm2-skeleton" style="animation-delay: ${i * 0.02}s">
20619 <span class="mm2-ico skeleton-box"></span> 20620 <span style="flex: 1;"> 20621 <div class="skeleton-line" style="width: 60%; height: 16px; margin-bottom: 6px;"></div> 20622 <div class="skeleton-line" style="width: 40%; height: 12px;"></div> 20623 </span> 20624 </div> 20625 `).join(''); 20626 topView.innerHTML = `<div class="mm2-top"><div class="mm2-grid">${skeletons}</div></div>`; 20627 } 20628 20629 // Count total products in a category tree 20630 function countProducts(node) { 20631 let count = node.product_count || 0; 20632 if (Array.isArray(node.children)) { 20633 node.children.forEach(child => { 20634 count += countProducts(child); 20635 }); 20636 } 20637 return count; 20638 } 20639 20640 // Cache for fetched thumbnails to avoid duplicate requests 20641 const thumbnailCache = new Map(); 20642 20643 // Fetch product thumbnails for a category 20644 function fetchCategoryThumbnails(categoryPath) { 20645 // Check cache first 20646 if (thumbnailCache.has(categoryPath)) { 20647 injectThumbnails(categoryPath, thumbnailCache.get(categoryPath)); 20648 return; 20649 } 20650 20651 const url = `/api/category_products_preview.php?path=${encodeURIComponent(categoryPath)}&limit=2`; 20652 20653 fetch(url) 20654 .then(r => { 20655 if (!r.ok) throw new Error('Network error'); 20656 return r.json(); 20657 }) 20658 .then(data => { 20659 if (data.success && data.products && data.products.length > 0) { 20660 thumbnailCache.set(categoryPath, data.products); 20661 injectThumbnails(categoryPath, data.products); 20662 } else { 20663 // No products, remove skeleton 20664 removeThumbSkeletons(categoryPath); 20665 } 20666 }) 20667 .catch(err => { 20668 console.warn('Failed to load thumbnails for', categoryPath, err); 20669 removeThumbSkeletons(categoryPath); 20670 }); 20671 } 20672 20673 // Inject thumbnails into the DOM 20674 function injectThumbnails(categoryPath, products) { 20675 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20676 links.forEach(link => { 20677 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20678 if (!thumbsContainer) return; 20679 20680 const thumbsHtml = products.slice(0, 2).map(p => 20681 `<img src="${esc(p.img)}" alt="${esc(p.name)}" class="mm2-thumb" title="${esc(p.name)}" loading="lazy">` 20682 ).join(''); 20683 20684 thumbsContainer.innerHTML = thumbsHtml; 20685 thumbsContainer.removeAttribute('data-loading'); 20686 }); 20687 } 20688 20689 // Remove skeleton loaders when no products 20690 function removeThumbSkeletons(categoryPath) { 20691 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20692 links.forEach(link => { 20693 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20694 if (thumbsContainer) { 20695 thumbsContainer.remove(); 20696 } 20697 }); 20698 } 20699 20700 function showDetail(node){ 20701 const title = node.label.split('/')[0]; 20702 const children = Array.isArray(node.children) ? node.children : []; 20703 const hasChildren = children.length > 0; 20704 20705 const subnav = hasChildren ? ` 20706 <nav class="mm2-subnav" role="tablist"> 20707 ${children.map((c2,i)=>{ 20708 const t = c2.label.split('/').slice(-1)[0]; 20709 return `<button class="mm2-subitem${i===0?' active':''}" role="tab" data-idx="${i}" aria-selected="${i===0?'true':'false'}">${esc(t)}</button>`; 20710 }).join('')} 20711 </nav>` : ''; 20712 20713 const subcontent = `<div class="mm2-subcontent" data-mm2="subcontent"></div>`; 20714 20715 detailView.innerHTML = ` 20716 <div class="mm2-detail"> 20717 <div class="mm2-detail-header"> 20718 <button class="mm2-back" type="button" data-mm2="back" aria-label="Kembali"> 20719 <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> 20720 </button> 20721 <div class="mm2-detail-title">${esc(title)}</div> 20722 </div> 20723 20724 ${hasChildren ? ` 20725 <div class="mm2-detail-layout"> 20726 ${subnav} 20727 ${subcontent} 20728 </div>` : ` 20729 <div class="mm2-links"> 20730 <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)}"> 20731 <strong>Lihat semua ${esc(title)}</strong> 20732 </a> 20733 </div> 20734 `} 20735 </div> 20736 `; 20737 20738 topView.classList.remove('is-active');
20739 detailView.classList.add('is-active'); 20740 20741 if (hasChildren) { 20742 const subEl = detailView.querySelector('.mm2-subnav'); 20743 const contEl = detailView.querySelector('[data-mm2="subcontent"]'); 20744 let activeIdx = 0; 20745 20746 const renderSubgrid = (idx) => { 20747 const c2 = children[idx]; if (!c2) { contEl.innerHTML=''; return; } 20748 const title2 = c2.label.split('/').slice(-1)[0]; 20749 const l3 = Array.isArray(c2.children) ? c2.children : []; 20750 const l3Html = l3.map(c3 => ` 20751 <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)}"> 20752 <span class="mm2-link-text">${esc(c3.label.split('/').slice(-1)[0])}</span> 20753 <span class="mm2-thumbs" data-loading="true"> 20754 <span class="mm2-thumb-skeleton"></span> 20755 <span class="mm2-thumb-skeleton"></span> 20756 </span> 20757 </a>`).join(''); 20758 20759 contEl.innerHTML = ` 20760 <div class="mm2-links"> 20761 <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)}"> 20762 <strong>Lihat semua ${esc(title2)}</strong> 20763 </a> 20764 ${l3Html} 20765 </div> 20766 `; 20767 20768 // Fetch product thumbnails for each level-3 category (desktop only) 20769 if (isDesktop()) { 20770 setTimeout(() => { 20771 l3.forEach(c3 => { 20772 fetchCategoryThumbnails(c3.path); 20773 }); 20774 }, 100); 20775 } 20776 }; 20777 20778 renderSubgrid(activeIdx); 20779 20780 // Desktop: hover ganti konten; Mobile & Desktop: click pilih 20781 subEl.addEventListener('mousemove', (e)=>{ 20782 if (!isDesktop()) return; 20783 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20784 const idx = +it.dataset.idx; if (!Number.isInteger(idx) || idx===activeIdx) return; 20785 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20786 it.classList.add('active'); 20787 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20788 activeIdx = idx; 20789 renderSubgrid(activeIdx); 20790 }); 20791 20792 subEl.addEventListener('click', (e)=>{ 20793 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20794 const idx = +it.dataset.idx; if (!Number.isInteger(idx)) return; 20795 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20796 it.classList.add('active'); 20797 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20798 activeIdx = idx; 20799 renderSubgrid(activeIdx); 20800 }); 20801 20802 // Delegate link clicks to add recent 20803 detailView.addEventListener('click', (e)=>{ 20804 const nav = e.target.closest('[data-mm2="nav"]'); 20805 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20806 }); 20807 } 20808 } 20809 20810 function buildTop(){ 20811 const html = data.map((n, idx) => { 20812 const title = n.label.split('/')[0]; 20813 const childCount = (n.children||[]).length; 20814 const productCount = countProducts(n); 20815 const gradient = categoryGradients[idx % categoryGradients.length]; 20816 20817 // Dynamic subtitle based on content - shorter for mobile 20818 let sub = ''; 20819 if (productCount > 0) { 20820 sub = `${productCount} produk`; 20821 if (childCount > 0) sub += ` · ${childCount} kat`; 20822 } else if (childCount > 0) { 20823 sub = `${childCount} kategori`; 20824 } else { 20825 sub = 'Lihat semua'; 20826 } 20827 20828 const iconStyle = `background: linear-gradient(135deg, ${gradient.from}, ${gradient.to}); border-color: ${gradient.border}; color: ${gradient.color};`; 20829 20830 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)}"> 20831 <span class="mm2-ico" style="${iconStyle}">${icon()}</span>
20832 <span style="flex: 1;"> 20833 <div class="mm2-title">${esc(title)}</div> 20834 <div class="mm2-sub">${esc(sub)}</div> 20835 </span> 20836 </a>`; 20837 }).join(''); 20838 const closeBtnHtml = !isDesktop() ? mobileCloseBtn() : ''; 20839 topView.innerHTML = `<div class="mm2-top">${closeBtnHtml}<div class="mm2-grid">${html}</div></div>`; 20840 } 20841 20842 // Open/close with safe-area 20843 trigger.addEventListener('click', (e)=>{ e.preventDefault(); document.body.classList.contains('mm2-open') ? close() : open(); }); 20844 trigger.addEventListener('pointerenter', ()=>{ if (isDesktop()) { clearTimeout(closeTimer); hoverTimer=setTimeout(open, 80); }}); 20845 trigger.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) { clearTimeout(hoverTimer); closeTimer=setTimeout(close, 200); }}); 20846 panel.addEventListener('pointerenter', ()=>{ if (isDesktop()) clearTimeout(closeTimer); }); 20847 panel.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) closeTimer=setTimeout(close, 200); }); 20848 backdrop?.addEventListener('pointerenter', ()=>{ clearTimeout(closeTimer); }); 20849 backdrop?.addEventListener('click', close); 20850 document.addEventListener('keydown', (e)=>{ if (e.key==='Escape') close(); }); 20851 20852 // Card click â open detail (unless cmd/ctrl to open link) 20853 panel.addEventListener('click', (e)=>{ 20854 const closeBtn = e.target.closest('[data-mm2="close"]'); if (closeBtn) { e.preventDefault(); close(); return; } 20855 const back = e.target.closest('[data-mm2="back"]'); if (back) { e.preventDefault(); showTop(); return; } 20856 const card = e.target.closest('[data-mm2="card"]'); 20857 if (card && !e.metaKey && !e.ctrlKey) { 20858 e.preventDefault(); 20859 const n = data.find(d => d.path === card.dataset.path); 20860 if (n) showDetail(n); 20861 } 20862 const nav = e.target.closest('[data-mm2="nav"]'); 20863 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20864 }); 20865 20866 // Swipe-down gesture for mobile (bottom sheet close) 20867 if (!isDesktop()) { 20868 let startY = 0, currentY = 0, isDragging = false, startTime = 0; 20869 const SWIPE_THRESHOLD = 80; 20870 const VELOCITY_THRESHOLD = 0.3; 20871 20872 panel.addEventListener('touchstart', (e) => { 20873 if (!document.body.classList.contains('mm2-open')) return; 20874 const touch = e.touches[0]; 20875 const scrollableContent = e.target.closest('.mm2-top, .mm2-detail'); 20876 20877 // Prevent drag if content is scrolled 20878 if (scrollableContent && scrollableContent.scrollTop > 5) return; 20879 20880 // Only start drag from top area or handle 20881 const rect = panel.getBoundingClientRect(); 20882 if (touch.clientY - rect.top > 60) return; 20883 20884 startY = touch.clientY; 20885 currentY = startY; 20886 startTime = Date.now(); 20887 isDragging = true; 20888 }, { passive: true }); 20889 20890 panel.addEventListener('touchmove', (e) => { 20891 if (!isDragging) return; 20892 const touch = e.touches[0]; 20893 currentY = touch.clientY; 20894 const deltaY = currentY - startY; 20895 20896 // Only allow downward swipe 20897 if (deltaY > 0) { 20898 const opacity = Math.max(0.4, 1 - (deltaY / 300)); 20899 panel.style.transform = `translateX(0) translateY(${deltaY}px)`; 20900 panel.style.opacity = opacity; 20901 } 20902 }, { passive: true }); 20903 20904 const endDrag = () => { 20905 if (!isDragging) return; 20906 const deltaY = currentY - startY; 20907 const deltaTime = Date.now() - startTime; 20908 const velocity = deltaY / deltaTime; 20909 20910 // Close if swiped down enough or fast swipe 20911 if (deltaY > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) { 20912 close(); 20913 } 20914 20915 // Reset position 20916 panel.style.transform = ''; 20917 panel.style.opacity = ''; 20918 isDragging = false; 20919 }; 20920 20921 panel.addEventListener('touchend', endDrag, { passive: true }); 20922 panel.addEventListener('touchcancel', endDrag, { passive: true }); 20923 } 20924 20925 // Fetch data with loading state 20926 if (!isLoading) { 20927 isLoading = true; 20928 showLoading(); 20929 20930 fetch(apiUrl, { headers: { 'Accept': 'application/json' }}) 20931 .then(r => { 20932 if (!r.ok) throw new Error('Network response was not ok'); 20933 return r.json(); 20934 }) 20935 .then(j => { 20936 data = Array.isArray(j.data) ? j.data : []; 20937 if (!data.length) { 20938 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20939 <p>Tidak ada kategori tersedia</p> 20940 </div></div>`; 20941 return; 20942 }
20943 buildTop(); 20944 isLoading = false; 20945 }) 20946 .catch(err => { 20947 console.error('Failed to load categories:', err); 20948 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20949 <p>Gagal memuat kategori. Silakan coba lagi.</p> 20950 </div></div>`; 20951 isLoading = false; 20952 }); 20953 } 20954})(); 20955 20956// Add skeleton animation styles 20957const style = document.createElement('style'); 20958style.textContent = ` 20959.mm2-skeleton { 20960 pointer-events: none; 20961 animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; 20962} 20963.skeleton-box, .skeleton-line { 20964 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 20965 background-size: 200% 100%; 20966 animation: shimmer 1.5s infinite; 20967 border-radius: 8px; 20968} 20969@keyframes pulse { 20970 0%, 100% { opacity: 1; } 20971 50% { opacity: 0.6; } 20972} 20973@keyframes shimmer { 20974 0% { background-position: 200% 0; } 20975 100% { background-position: -200% 0; } 20976} 20977`; 20978document.head.appendChild(style); 20979</script>
20979 20980
20980<script data-inlined="benchmark.js"> 20981/** 20982 * Benchmark Data Loader 20983 * Handles AJAX fetch and display of benchmark data 20984 */ 20985 20986(function() { 20987 'use strict'; 20988 20989 // Wait for DOM ready 20990 if (document.readyState === 'loading') { 20991 document.addEventListener('DOMContentLoaded', initBenchmark); 20992 } else { 20993 initBenchmark(); 20994 } 20995 20996 function initBenchmark() { 20997 const section = document.querySelector('.pd-benchmark-section'); 20998 if (!section) return; 20999 21000 const button = document.getElementById('btn-load-benchmark'); 21001 const content = document.getElementById('benchmark-content'); 21002 const productId = section.dataset.productId; 21003 21004 if (!button || !content || !productId) return; 21005 21006 let isLoading = false; 21007 let dataLoaded = false; 21008 21009 button.addEventListener('click', function() { 21010 if (isLoading || dataLoaded) return; 21011 21012 loadBenchmarkData(productId, button, content); 21013 }); 21014 } 21015 21016 function loadBenchmarkData(productId, button, container) { 21017 // Update button state 21018 button.disabled = true; 21019 button.classList.add('loading'); 21020 const originalText = button.querySelector('.btn-text').textContent; 21021 button.querySelector('.btn-text').textContent = 'Memuat data...'; 21022 21023 // Show loading state 21024 container.style.display = 'block'; 21025 container.innerHTML = ` 21026 <div class="benchmark-loading"> 21027 <div class="benchmark-loading-spinner"></div> 21028 <p>Mengambil data benchmark...</p> 21029 </div> 21030 `; 21031 21032 // Fetch data 21033 fetch('/api/benchmark/fetch.php?product_id=' + productId) 21034 .then(function(response) { 21035 if (!response.ok) { 21036 throw new Error('HTTP error ' + response.status); 21037 } 21038 return response.json(); 21039 }) 21040 .then(function(result) { 21041 if (result.success && result.data) { 21042 renderBenchmarkData(result.data, container); 21043 button.style.display = 'none'; // Hide button after successful load 21044 } else { 21045 throw new Error(result.error || 'Data tidak tersedia'); 21046 } 21047 }) 21048 .catch(function(error) { 21049 console.error('Benchmark fetch error:', error); 21050 container.innerHTML = ` 21051 <div class="benchmark-error"> 21052 <p><strong>â ï¸ Data Benchmark Tidak Tersedia</strong></p> 21053 <p style="font-size: 14px; margin-top: 8px;"> 21054 ${error.message || 'Terjadi kesalahan saat memuat data'} 21055 </p> 21056 <button type="button" onclick="location.reload()" 21057 style="margin-top: 16px; padding: 8px 20px; background: #667eea; color: #fff; border: none; border-radius: 6px; cursor: pointer;"> 21058 Coba Lagi 21059 </button> 21060 </div> 21061 `; 21062 21063 // Reset button 21064 button.disabled = false; 21065 button.classList.remove('loading'); 21066 button.querySelector('.btn-text').textContent = originalText; 21067 }); 21068 } 21069 21070 function renderBenchmarkData(data, container) { 21071 let html = '<div class="benchmark-data-wrapper">'; 21072 21073 // Chipset info if available 21074 if (data.chipset) { 21075 html += ` 21076 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px 16px; border-radius: 8px; margin-bottom: 12px;"> 21077 <div style="font-size: 11px; opacity: 0.85; margin-bottom: 2px;">Chipset</div> 21078 <div style="font-size: 15px; font-weight: 600;">${escapeHtml(data.chipset)}</div> 21079 </div> 21080 `; 21081 } 21082 21083 // Main benchmark scores 21084 if (data.antutu || data.geekbench_multi || data.overall_score) { 21085 html += '<div class="benchmark-data-grid">'; 21086 21087 if (data.antutu) { 21088 html += ` 21089 <div class="benchmark-card"> 21090 <div class="benchmark-card-label">ANTUTU</div> 21091 <div class="benchmark-card-value large">${formatNumber(data.antutu)}</div> 21092 ${getScoreBadge(data.antutu, 'antutu')} 21093 </div> 21094 `; 21095 } 21096 21097 if (data.geekbench_multi) { 21098 html += ` 21099 <div class="benchmark-card"> 21100 <div class="benchmark-card-label">GEEKBENCH MULTI</div> 21101 <div class="benchmark-card-value">${formatNumber(data.geekbench_multi)}</div> 21102 ${data.geekbench_single ? '<div class="benchmark-card-subtitle">Single: ' + formatNumber(data.geekbench_single) + '</div>' : ''} 21103 </div> 21104 `; 21105 } 21106 21107 if (data.gaming_score) { 21108 html += ` 21109 <div class="benchmark-card"> 21110 <div class="benchmark-card-label">GAMING</div> 21111 <div class="benchmark-card-value">${data.gaming_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21112 </div> 21113 `; 21114 } 21115 21116 if (data.battery_score) { 21117 html += ` 21118 <div class="benchmark-card"> 21119 <div class="benchmark-card-label">BATTERY</div> 21120 <div class="benchmark-card-value">${data.battery_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21121 </div> 21122 `; 21123 } 21124 21125 if (data.overall_score) { 21126 html += ` 21127 <div class="benchmark-card"> 21128 <div class="benchmark-card-label">OVERALL</div> 21129 <div class="benchmark-card-value">${data.overall_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21130 </div> 21131 `; 21132 } 21133 21134 html += '</div>'; 21135 } 21136 21137 // AI Performance Description 21138 if (data.notes) { 21139 html += ` 21140 <div style="background: #f8f9fa; border-left: 3px solid #667eea; padding: 10px 14px; border-radius: 6px; margin-top: 12px;"> 21141 <div style="font-size: 11px; font-weight: 600; color: #667eea; margin-bottom: 6px;">ð¡ Analisis Performa</div> 21142 <div style="font-size: 13px; color: #4a5568; line-height: 1.5;">${escapeHtml(data.notes)}</div> 21143 </div> 21144 `; 21145 } 21146 21147 // Attribution 21148 html += ` 21149 <div class="benchmark-attribution"> 21150 <p style="font-size: 11px; margin: 8px 0 0 0; color: #888;">Data benchmark dianalisis menggunakan AI dari spesifikasi produk</p> 21151 ${data.fetched_at ? '<p style="font-size: 10px; margin: 4px 0 0 0; color: #aaa;">Terakhir diperbarui: ' + formatDate(data.fetched_at) + '</p>' : ''} 21152 </div> 21153 `; 21154 21155 html += '</div>'; 21156 21157 container.innerHTML = html; 21158 } 21159 21160 function formatNumber(num) { 21161 if (!num) return '-'; 21162 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 21163 } 21164 21165 function escapeHtml(text) { 21166 if (!text) return ''; 21167 const div = document.createElement('div'); 21168 div.textContent = text; 21169 return div.innerHTML; 21170 } 21171 21172 function formatDate(dateStr) { 21173 try { 21174 const date = new Date(dateStr); 21175 const options = { year: 'numeric', month: 'long', day: 'numeric' }; 21176 return date.toLocaleDateString('id-ID', options); 21177 } catch (e) { 21178 return dateStr; 21179 } 21180 } 21181 21182 function getScoreBadge(score, type) { 21183 let category = ''; 21184 let label = ''; 21185 21186 if (type === 'antutu') { 21187 if (score >= 500000) { 21188 category = 'excellent'; 21189 label = 'Flagship'; 21190 } else if (score >= 350000) { 21191 category = 'good'; 21192 label = 'High-End'; 21193 } else if (score >= 200000) { 21194 category = 'average'; 21195 label = 'Mid-Range'; 21196 } else { 21197 category = 'average'; 21198 label = 'Entry-Level'; 21199 } 21200 } 21201 21202 if (label) { 21203 return '<div class="benchmark-score-badge ' + category + '">' + label + '</div>'; 21204 } 21205 21206 return ''; 21207 } 21208})(); 21209 21210</script>
21210 21211
21211<script data-inlined="product-compare.js"> 21212(function() { 21213 'use strict'; 21214 21215 // State management 21216 let compareList = []; 21217 let isAnalyzing = false; 21218 let currentAnalysis = null; 21219 21220 // DOM elements 21221 const compareBtn = document.getElementById('compareBtn'); 21222 const compareSidebar = document.getElementById('compareSidebar'); 21223 const compareOverlay = document.getElementById('compareOverlay'); 21224 const compareClose = document.getElementById('compareClose'); 21225 const compareModal = document.getElementById('aiCompareModal'); 21226 const compareModalClose = document.getElementById('aiCompareModalClose'); 21227 21228 // Initialize 21229 document.addEventListener('DOMContentLoaded', async function() { 21230 await initializeCompare(); 21231 setupEventListeners(); 21232 }); 21233 21234 /** 21235 * Initialize compare functionality 21236 */ 21237 async function initializeCompare() { 21238 // Load compare list first before setting up buttons 21239 await loadCompareList(); 21240 21241 // Setup product detail compare button if exists 21242 const productCompareBtn = document.getElementById('pd-compare-btn'); 21243 21244 if (productCompareBtn) { 21245 setupProductCompareButton(productCompareBtn); 21246 } 21247 21248 // Check for existing compare list and update UI 21249 if (compareList.length > 0) { 21250 // Only update badge and buttons, no sticky bar 21251 } 21252 } 21253 21254 /** 21255 * Setup event listeners 21256 */ 21257 function setupEventListeners() { 21258 // Navigation compare button 21259 if (compareBtn) { 21260 compareBtn.addEventListener('click', toggleCompareSidebar); 21261 } 21262 21263 // Sidebar controls 21264 if (compareClose) { 21265 compareClose.addEventListener('click', closeCompareSidebar); 21266 } 21267 21268 if (compareOverlay) { 21269 compareOverlay.addEventListener('click', closeCompareSidebar); 21270 } 21271 21272 // Sidebar actions 21273 const clearAllBtn = document.getElementById('compareClearAll'); 21274 if (clearAllBtn) { 21275 clearAllBtn.addEventListener('click', clearAllProducts); 21276 } 21277 21278 const analyzeBtn = document.getElementById('compareAnalyze'); 21279 if (analyzeBtn) { 21280 analyzeBtn.addEventListener('click', startAIAnalysis); 21281 } 21282 21283 // Modal controls 21284 if (compareModalClose) { 21285 compareModalClose.addEventListener('click', closeModal); 21286 } 21287 21288 const modalBackdrop = compareModal?.querySelector('.modal-backdrop'); 21289 if (modalBackdrop) { 21290 modalBackdrop.addEventListener('click', closeModal); 21291 } 21292 21293 // Modal actions 21294 const retryBtn = document.getElementById('aiRetry'); 21295 if (retryBtn) { 21296 retryBtn.addEventListener('click', startAIAnalysis); 21297 } 21298 21299 // Removed share and add to cart buttons - replaced with AI disclaimer 21300 } 21301 21302 /** 21303 * Setup product detail compare button 21304 */ 21305 function setupProductCompareButton(button) { 21306 const productId = parseInt(button.dataset.productId); 21307 if (!productId) return; 21308 21309 // Check if product is already in compare list 21310 updateProductCompareButtonState(button, productId); 21311 21312 button.addEventListener('click', async function() { 21313 // Get current variant ID (if any) 21314 const variantIdElement = document.getElementById('pd-variant-id'); 21315 const variantId = variantIdElement ? parseInt(variantIdElement.value) || 0 : 0; 21316 21317 // Update button's variant data for current selection 21318 button.dataset.variantId = variantId.toString(); 21319 21320 await toggleProductInCompare(productId, button, variantId); 21321 }); 21322 } 21323 21324 /** 21325 * Toggle product in compare list 21326 */ 21327 async function toggleProductInCompare(productId, button, variantId = 0) { 21328 // Check by product ID (variants of same product treated as same item) 21329 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21330 21331 try { 21332 if (isInCompare) { 21333 await removeFromCompare(productId); 21334 showToast('Produk dihapus dari perbandingan', 'success'); 21335 } else { 21336 await addToCompare(productId, variantId); 21337 showToast('Produk ditambahkan ke perbandingan', 'success'); 21338 21339 // Trigger fly-to-compare animation 21340 triggerFlyToCompareAnimation(button); 21341 } 21342 21343 updateProductCompareButtonState(button, productId); 21344 updateCompareBadge(); 21345 updateAllCompareButtons(); 21346 21347 } catch (error) { 21348 showToast(error.message || 'Terjadi kesalahan', 'error'); 21349 } 21350 } 21351 21352 /** 21353 * Update product compare button state 21354 */ 21355 function updateProductCompareButtonState(button, productId) { 21356 // Ensure both IDs are compared as integers 21357 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21358 button.classList.toggle('added', isInCompare); 21359 21360 // Handle visibility of text elements 21361 const compareText = button.querySelector('.compare-text'); 21362 const compareRemove = button.querySelector('.compare-remove'); 21363 const compareNote = document.getElementById('compare-note'); 21364 21365 if (compareText) { 21366 compareText.style.display = isInCompare ? 'none' : 'inline'; 21367 } 21368 21369 if (compareRemove) { 21370 if (isInCompare) { 21371 compareRemove.removeAttribute('hidden'); 21372 compareRemove.style.display = 'flex'; 21373 } else { 21374 compareRemove.setAttribute('hidden', ''); 21375 compareRemove.style.display = 'none'; 21376 } 21377 } 21378 21379 // Update info note 21380 if (compareNote) { 21381 if (isInCompare) { 21382 const currentCount = compareList.length; 21383 compareNote.textContent = `Produk sudah ada di daftar perbandingan (${currentCount}/5). Klik tombol untuk menghapus.`; 21384 } else { 21385 compareNote.textContent = 'Bandingkan hingga 5 produk dengan bantuan AI'; 21386 } 21387 } 21388 21389 // Inline compare preview removed - keeping it simple 21390 } 21391 21392 /** 21393 * Add product to compare list 21394 */ 21395 async function addToCompare(productId, variantId = 0) { 21396 const requestData = { product_id: productId }; 21397 if (variantId > 0) { 21398 requestData.variant_id = variantId; 21399 } 21400 21401 const response = await fetch('/api/compare/add.php', { 21402 method: 'POST', 21403 headers: { 21404 'Content-Type': 'application/json', 21405 }, 21406 body: JSON.stringify(requestData), 21407 credentials: 'same-origin' 21408 }); 21409 21410 const result = await response.json(); 21411 21412 if (!result.success) { 21413 throw new Error(result.error || 'Gagal menambahkan produk'); 21414 } 21415 21416 // Send Google Analytics event for add to compare 21417 if (typeof gtag === 'function') { 21418 gtag('event', 'add_to_compare', { 21419 event_category: 'Product Compare', 21420 event_label: 'Product Added to Compare', 21421 product_id: productId, 21422 variant_id: variantId || 0, 21423 custom_map: { 21424 custom_parameter_1: 'compare_add_product' 21425 } 21426 }); 21427 } 21428
21429 await loadCompareList(); 21430 return result; 21431 } 21432 21433 /** 21434 * Remove product from compare list 21435 */ 21436 async function removeFromCompare(productId) { 21437 const response = await fetch('/api/compare/remove.php', { 21438 method: 'POST', 21439 headers: { 21440 'Content-Type': 'application/json', 21441 }, 21442 body: JSON.stringify({ product_id: productId }), 21443 credentials: 'same-origin' 21444 }); 21445 21446 const result = await response.json(); 21447 21448 if (!result.success) { 21449 throw new Error(result.error || 'Gagal menghapus produk'); 21450 } 21451 21452 // Send Google Analytics event for remove from compare 21453 if (typeof gtag === 'function') { 21454 gtag('event', 'remove_from_compare', { 21455 event_category: 'Product Compare', 21456 event_label: 'Product Removed from Compare', 21457 product_id: productId, 21458 custom_map: { 21459 custom_parameter_1: 'compare_remove_product' 21460 } 21461 }); 21462 } 21463 21464 await loadCompareList(); 21465 return result; 21466 } 21467 21468 /** 21469 * Clear all products from compare list 21470 */ 21471 async function clearAllProducts() { 21472 if (compareList.length === 0) return; 21473 21474 // Show confirmation modal 21475 const confirmed = await showConfirmModal({ 21476 title: 'Hapus Semua Produk', 21477 message: 'Apakah Anda yakin ingin menghapus semua produk dari daftar perbandingan?', 21478 confirmText: 'Hapus Semua', 21479 cancelText: 'Batal', 21480 type: 'danger' 21481 }); 21482 21483 if (!confirmed) return; 21484 21485 try { 21486 const response = await fetch('/api/compare/clear.php', { 21487 method: 'POST', 21488 credentials: 'same-origin' 21489 }); 21490 21491 const result = await response.json(); 21492 21493 if (!result.success) { 21494 throw new Error(result.error || 'Gagal menghapus produk'); 21495 } 21496 21497 // Send Google Analytics event for clear all products 21498 if (typeof gtag === 'function') { 21499 gtag('event', 'compare_clear_all', { 21500 event_category: 'Product Compare', 21501 event_label: 'Clear All Products', 21502 products_count: compareList.length, 21503 custom_map: { 21504 custom_parameter_1: 'compare_clear_action' 21505 } 21506 }); 21507 } 21508 21509 await loadCompareList(); 21510 showToast('Semua produk dihapus dari perbandingan', 'success'); 21511 21512 } catch (error) { 21513 showToast(error.message || 'Terjadi kesalahan', 'error'); 21514 } 21515 } 21516 21517 /** 21518 * Load compare list from server 21519 */ 21520 async function loadCompareList() { 21521 try { 21522 const response = await fetch('/api/compare/get.php', { 21523 credentials: 'same-origin' 21524 }); 21525 21526 const result = await response.json(); 21527 21528 if (result.success) { 21529 compareList = result.products || []; 21530 updateCompareSidebar(); 21531 updateCompareBadge(); 21532 updateAllCompareButtons(); 21533 } 21534 21535 } catch (error) { 21536 // Silently handle error - compare functionality will work with empty list 21537 } 21538 } 21539 21540 /** 21541 * Update compare sidebar content 21542 */ 21543 function updateCompareSidebar() { 21544 const emptyState = document.getElementById('compareEmpty'); 21545 const listContainer = document.getElementById('compareList'); 21546 const actionsContainer = document.getElementById('compareActions'); 21547 const countElement = document.getElementById('compareCountSidebar'); 21548 21549 if (!emptyState || !listContainer || !actionsContainer || !countElement) return; 21550 21551 // Update count 21552 countElement.textContent = `(${compareList.length})`; 21553 21554 if (compareList.length === 0) { 21555 emptyState.style.display = 'block'; 21556 listContainer.style.display = 'none'; 21557 actionsContainer.style.display = 'none'; 21558 } else { 21559 emptyState.style.display = 'none'; 21560 listContainer.style.display = 'block'; 21561 actionsContainer.style.display = 'flex'; 21562 21563 // Render product list 21564 listContainer.innerHTML = compareList.map(product => { 21565 // Display variant name if available 21566 let displayName = product.name; 21567 if (product.combination_name) { 21568 displayName += ` (${product.combination_name})`; 21569 } 21570 21571 // Use variant price if available 21572 let displayPrice = product.price; 21573 if (product.variant_price !== undefined && product.variant_price !== null) { 21574 displayPrice = product.variant_price; 21575 } 21576 21577 return ` 21578 <div class="compare-item" data-product-id="${product.id}"> 21579 <div class="compare-item-image"> 21580 <img src="${product.image_url}" alt="${escapeHtml(displayName)}" loading="lazy"> 21581 </div> 21582 <div class="compare-item-info"> 21583 <div class="compare-item-name">${escapeHtml(displayName)}</div> 21584 <div class="compare-item-price">${formatPrice(displayPrice)}</div> 21585 </div> 21586 <button class="compare-item-remove" data-product-id="${product.id}" aria-label="Hapus">
21587 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 21588 <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"/> 21589 </svg> 21590 </button> 21591 </div> 21592 `; 21593 }).join(''); 21594 21595 // Setup event listeners for remove buttons 21596 setupRemoveButtons(); 21597 21598 // Update analyze button state 21599 const analyzeBtn = document.getElementById('compareAnalyze'); 21600 if (analyzeBtn) { 21601 analyzeBtn.disabled = compareList.length < 2; 21602 } 21603 } 21604 } 21605 21606 /** 21607 * Setup remove buttons event listeners 21608 */ 21609 function setupRemoveButtons() { 21610 const removeButtons = document.querySelectorAll('.compare-item-remove'); 21611 removeButtons.forEach(button => { 21612 button.addEventListener('click', async (e) => { 21613 e.preventDefault(); 21614 const productId = parseInt(button.dataset.productId); 21615 if (productId) { 21616 await removeProduct(productId); 21617 } 21618 }); 21619 }); 21620 } 21621 21622 /** 21623 * Update all compare buttons on the page 21624 */ 21625 function updateAllCompareButtons() { 21626 // Update product detail compare button 21627 const productBtn = document.getElementById('pd-compare-btn'); 21628 if (productBtn) { 21629 const productId = parseInt(productBtn.dataset.productId); 21630 if (productId) { 21631 updateProductCompareButtonState(productBtn, productId); 21632 } 21633 } 21634 21635 // Update any other compare buttons on the page 21636 const allCompareButtons = document.querySelectorAll('[data-product-id][class*="compare"]'); 21637 allCompareButtons.forEach(button => { 21638 const productId = parseInt(button.dataset.productId); 21639 if (productId && button.id !== 'pd-compare-btn') { 21640 updateProductCompareButtonState(button, productId); 21641 } 21642 }); 21643 } 21644 21645 /** 21646 * Update compare badge 21647 */ 21648 function updateCompareBadge() { 21649 const badge = document.getElementById('compareCount'); 21650 if (!badge) return; 21651 21652 const oldCount = parseInt(badge.textContent) || 0; 21653 const newCount = compareList.length; 21654 21655 badge.textContent = newCount; 21656 badge.classList.toggle('active', newCount > 0); 21657 21658 // Animate badge if count increased 21659 if (newCount > oldCount) { 21660 animateCompareBadge(); 21661 highlightCompareButton(); 21662 } 21663 21664 // Sticky bar removed - keeping only fly-to-navbar animation 21665 } 21666 21667 /** 21668 * Animate compare badge when new product added 21669 */ 21670 function animateCompareBadge() { 21671 const badge = document.getElementById('compareCount'); 21672 if (!badge) return; 21673 21674 badge.style.transform = 'scale(1.3)'; 21675 badge.style.transition = 'transform 0.3s ease'; 21676 21677 setTimeout(() => { 21678 badge.style.transform = 'scale(1)'; 21679 }, 300); 21680 } 21681 21682 /** 21683 * Highlight compare button temporarily 21684 */ 21685 function highlightCompareButton() { 21686 const compareBtn = document.getElementById('compareBtn'); 21687 if (!compareBtn) return; 21688 21689 compareBtn.classList.add('pulse-highlight'); 21690 21691 setTimeout(() => { 21692 compareBtn.classList.remove('pulse-highlight'); 21693 }, 3000); 21694 } 21695 21696 /** 21697 * Trigger fly-to-compare animation 21698 */ 21699 function triggerFlyToCompareAnimation(sourceButton) { 21700 if (!sourceButton) { 21701 return; 21702 } 21703 21704 // Check if mobile view (bottom nav) or desktop (top nav) 21705 const isMobile = window.innerWidth <= 768; 21706 21707 // Get source position 21708 const sourceRect = sourceButton.getBoundingClientRect(); 21709 21710 // Calculate target position 21711 let targetTop, targetLeft; 21712 21713 if (isMobile) { 21714 // Mobile: fly to fixed bottom position (bottom nav area) 21715 // Use fixed coordinates regardless of bottom nav visibility 21716 targetTop = window.innerHeight - 80; // Just above bottom nav area 21717 targetLeft = window.innerWidth / 2 - 30; // Center horizontally 21718 } else { 21719 // Desktop: fly to top navbar compare button 21720 const targetElement = document.getElementById('compareBtn'); 21721 if (!targetElement) { 21722 return; 21723 } 21724 const targetRect = targetElement.getBoundingClientRect(); 21725 targetTop = targetRect.top + 10; 21726 targetLeft = targetRect.left + 10; 21727 } 21728 21729 // Get product image for animation - try multiple selectors 21730 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"]'); 21731 21732 // Create animated element 21733 const flyingElement = document.createElement('div'); 21734 flyingElement.className = 'flying-product'; 21735 21736 if (productImage) { 21737 flyingElement.innerHTML = `<img src="${productImage.src}" alt="Flying product">`; 21738 flyingElement.querySelector('img').style.cssText = `
21739 width: 100%; 21740 height: 100%; 21741 object-fit: cover; 21742 border-radius: 8px; 21743 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21744 `; 21745 } else { 21746 // Fallback: colored div with icon 21747 flyingElement.innerHTML = `<div class="flying-fallback">ð¦</div>`; 21748 flyingElement.querySelector('.flying-fallback').style.cssText = ` 21749 width: 100%; 21750 height: 100%; 21751 background: #3b82f6; 21752 border-radius: 8px; 21753 display: flex; 21754 align-items: center; 21755 justify-content: center; 21756 font-size: 24px; 21757 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21758 `; 21759 } 21760 21761 flyingElement.style.cssText = ` 21762 position: fixed; 21763 top: ${sourceRect.top}px; 21764 left: ${sourceRect.left}px; 21765 width: 60px; 21766 height: 60px; 21767 z-index: 9999; 21768 pointer-events: none; 21769 opacity: 1; 21770 transform: scale(1); 21771 `; 21772 21773 document.body.appendChild(flyingElement); 21774 21775 // Use Web Animations API for more reliable animation 21776 flyingElement.animate([ 21777 // Keyframe 1: Initial state 21778 { 21779 top: `${sourceRect.top}px`, 21780 left: `${sourceRect.left}px`, 21781 transform: 'scale(1)', 21782 opacity: 1 21783 }, 21784 // Keyframe 2: Final state 21785 { 21786 top: `${targetTop}px`, 21787 left: `${targetLeft}px`, 21788 transform: 'scale(0.3)', 21789 opacity: 0 21790 } 21791 ], { 21792 duration: 800, 21793 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 21794 fill: 'forwards' 21795 }); 21796 21797 // Clean up after animation completes 21798 setTimeout(() => { 21799 if (flyingElement.parentNode) { 21800 flyingElement.parentNode.removeChild(flyingElement); 21801 } 21802 }, 850); 21803 } 21804 21805 // Sticky compare bar functions removed - using only fly-to-navbar animation 21806 21807 /** 21808 * Toggle compare sidebar 21809 */ 21810 function toggleCompareSidebar() { 21811 if (!compareSidebar) return; 21812 21813 const isOpen = compareSidebar.classList.contains('open'); 21814 21815 if (isOpen) { 21816 closeCompareSidebar(); 21817 } else { 21818 openCompareSidebar(); 21819 } 21820 } 21821 21822 /** 21823 * Open compare sidebar 21824 */ 21825 function openCompareSidebar() { 21826 if (!compareSidebar || !compareOverlay) return; 21827 21828 compareSidebar.classList.add('open'); 21829 compareOverlay.classList.add('active'); 21830 document.body.style.overflow = 'hidden'; 21831 21832 // Send Google Analytics event for compare sidebar open 21833 if (typeof gtag === 'function') { 21834 gtag('event', 'compare_sidebar_open', { 21835 event_category: 'Product Compare', 21836 event_label: 'Compare Sidebar Opened', 21837 products_count: compareList.length, 21838 custom_map: { 21839 custom_parameter_1: 'compare_sidebar_view' 21840 } 21841 }); 21842 } 21843 } 21844 21845 /** 21846 * Close compare sidebar 21847 */ 21848 function closeCompareSidebar() { 21849 if (!compareSidebar || !compareOverlay) return; 21850 21851 compareSidebar.classList.remove('open'); 21852 compareOverlay.classList.remove('active'); 21853 document.body.style.overflow = ''; 21854 } 21855 21856 /** 21857 * Start AI analysis 21858 */ 21859 async function startAIAnalysis() { 21860 if (compareList.length < 2) { 21861 showToast('Minimal 2 produk diperlukan untuk analisis AI', 'warning'); 21862 return; 21863 } 21864 21865 if (isAnalyzing) return; 21866 21867 // Send Google Analytics event for AI Analysis start 21868 if (typeof gtag === 'function') { 21869 gtag('event', 'ai_analysis_start', { 21870 event_category: 'Product Compare', 21871 event_label: 'AI Analysis Button Click', 21872 products_count: compareList.length, 21873 product_ids: compareList.map(p => p.id).join(','), 21874 custom_map: { 21875 custom_parameter_1: 'compare_ai_analysis' 21876 } 21877 }); 21878 } 21879 21880 // Check if products are in similar categories 21881 const mismatchInfo = await checkProductCategoryMismatch(); 21882 if (mismatchInfo.shouldWarn) { 21883 const categoryList = Array.from(mismatchInfo.categories).join(', '); 21884 const confirmed = await showConfirmModal({ 21885 title: 'Produk Berbeda Kategori', 21886 message: `Produk yang Anda pilih berasal dari kategori yang berbeda: ${categoryList}. AI tetap bisa menganalisis, tapi perbandingan mungkin kurang relevan. Lanjutkan analisis?`, 21887 confirmText: 'Ya, Analisis Tetap', 21888 cancelText: 'Batal', 21889 type: 'warning' 21890 }); 21891 21892 if (!confirmed) return; 21893 } 21894 21895 openModal(); 21896 showLoadingState(); 21897 isAnalyzing = true; 21898 21899 try { 21900 const response = await fetch('/api/compare/ai-analysis.php', { 21901 method: 'POST', 21902 credentials: 'same-origin' 21903 }); 21904 21905 const result = await response.json(); 21906 21907 if (!result.success) { 21908 throw new Error(result.error || 'Analisis AI gagal'); 21909 } 21910 21911 currentAnalysis = result; 21912 showAnalysisResults(result); 21913 21914 // Send Google Analytics event for successful AI Analysis 21915 if (typeof gtag === 'function') { 21916 gtag('event', 'ai_analysis_success', { 21917 event_category: 'Product Compare', 21918 event_label: 'AI Analysis Completed', 21919 products_count: compareList.length, 21920 analysis_type: 'product_comparison', 21921 custom_map: { 21922 custom_parameter_1: 'compare_ai_success' 21923 } 21924 }); 21925 } 21926 21927 } catch (error) { 21928 showErrorState(error.message); 21929 21930 // Send Google Analytics event for failed AI Analysis 21931 if (typeof gtag === 'function') { 21932 gtag('event', 'ai_analysis_error', { 21933 event_category: 'Product Compare', 21934 event_label: 'AI Analysis Failed', 21935 products_count: compareList.length, 21936 error_message: error.message, 21937 custom_map: { 21938 custom_parameter_1: 'compare_ai_error' 21939 } 21940 }); 21941 } 21942 } finally { 21943 isAnalyzing = false;
21944 } 21945 } 21946 21947 /** 21948 * Show modal 21949 */ 21950 function openModal() { 21951 if (!compareModal) return; 21952 compareModal.classList.add('open'); 21953 document.body.style.overflow = 'hidden'; 21954 } 21955 21956 /** 21957 * Close modal 21958 */ 21959 function closeModal() { 21960 if (!compareModal) return; 21961 compareModal.classList.remove('open'); 21962 document.body.style.overflow = ''; 21963 } 21964 21965 /** 21966 * Show loading state 21967 */ 21968 function showLoadingState() { 21969 const loading = document.getElementById('aiLoading'); 21970 const error = document.getElementById('aiError'); 21971 const results = document.getElementById('aiResults'); 21972 21973 if (loading) loading.style.display = 'block'; 21974 if (error) error.hidden = true; 21975 if (results) results.hidden = true; 21976 } 21977 21978 /** 21979 * Show error state 21980 */ 21981 function showErrorState(message) { 21982 const loading = document.getElementById('aiLoading'); 21983 const error = document.getElementById('aiError'); 21984 const results = document.getElementById('aiResults'); 21985 const errorMessage = document.getElementById('aiErrorMessage'); 21986 21987 if (loading) loading.style.display = 'none'; 21988 if (error) error.hidden = false; 21989 if (results) results.hidden = true; 21990 if (errorMessage) errorMessage.textContent = message; 21991 } 21992 21993 /** 21994 * Show analysis results 21995 */ 21996 function showAnalysisResults(analysis) { 21997 const loading = document.getElementById('aiLoading'); 21998 const error = document.getElementById('aiError'); 21999 const results = document.getElementById('aiResults'); 22000 22001 if (loading) loading.style.display = 'none'; 22002 if (error) error.hidden = true; 22003 if (results) results.hidden = false; 22004 22005 const data = analysis.data; 22006 22007 // Update summary 22008 const summaryElement = document.getElementById('aiSummary'); 22009 if (summaryElement) summaryElement.textContent = data.summary || ''; 22010 22011 const winnerProduct = document.getElementById('winnerProduct'); 22012 const winnerReason = document.getElementById('winnerReason'); 22013 if (winnerProduct && winnerReason && data.winner_overall) { 22014 const winner = compareList.find(p => parseInt(p.id) === parseInt(data.winner_overall)); 22015 winnerProduct.textContent = winner ? winner.name : 'Tidak diketahui'; 22016 winnerReason.textContent = data.winner_reason || ''; 22017 } 22018 22019 // Update comparison table 22020 updateComparisonTable(data.comparison_table); 22021 22022 // Update detailed analysis 22023 updateDetailedAnalysis(data.detailed_analysis); 22024 22025 // Update recommendations 22026 updateRecommendations(data.recommendations); 22027 22028 // Update buying tips 22029 updateBuyingTips(data.buying_tips); 22030 } 22031 22032 /** 22033 * Update comparison table 22034 */ 22035 function updateComparisonTable(comparisonData) { 22036 const container = document.getElementById('comparisonTable'); 22037 if (!container || !comparisonData?.categories) return; 22038 22039 // First, create product header row with names 22040 const productHeaderHtml = ` 22041 <div class="comparison-category"> 22042 <div class="category-header">Produk</div> 22043 <div class="category-products"> 22044 ${compareList.map(product => ` 22045 <div class="product-score"> 22046 <div class="product-name-header"> 22047 <strong>${escapeHtml(product.name)}</strong> 22048 </div> 22049 <div class="product-price"> 22050 ${formatPrice(product.price)} 22051 </div> 22052 </div> 22053 `).join('')} 22054 </div> 22055 </div> 22056 `; 22057 22058 // Then create comparison categories 22059 const categoriesHtml = comparisonData.categories.map(category => ` 22060 <div class="comparison-category"> 22061 <div class="category-header">${escapeHtml(category.name)}</div> 22062 <div class="category-products"> 22063 ${compareList.map(product => { 22064 const productData = category.products?.[product.id]; 22065 return ` 22066 <div class="product-score"> 22067 <div class="score-value"> 22068 <span class="score-stars">${'â '.repeat(productData?.score || 0)}${'â'.repeat(5 - (productData?.score || 0))}</span> 22069 <span>${productData?.score || 0}/5</span> 22070 </div> 22071 <div class="score-note">${escapeHtml(productData?.note || '')}</div> 22072 </div> 22073 `; 22074 }).join('')} 22075 </div> 22076 </div> 22077 `).join(''); 22078 22079 container.innerHTML = productHeaderHtml + categoriesHtml; 22080 } 22081 22082 /** 22083 * Update detailed analysis 22084 */ 22085 function updateDetailedAnalysis(analysisData) { 22086 const container = document.getElementById('detailedAnalysis'); 22087 if (!container || !analysisData) return; 22088 22089 container.innerHTML = compareList.map(product => { 22090 const productAnalysis = analysisData[product.id]; 22091 if (!productAnalysis) return ''; 22092 22093 // Use product name from API response if available, fallback to local name 22094 const productName = productAnalysis.product_name || product.name; 22095 22096 return ` 22097 <div class="product-analysis"> 22098 <h5>${escapeHtml(productName)}</h5> 22099 22100 ${productAnalysis.strengths ? ` 22101 <div class="analysis-section"> 22102 <h6 class="strengths">â Kelebihan</h6> 22103 <ul> 22104 ${productAnalysis.strengths.map(strength => 22105 `<li>${escapeHtml(strength)}</li>` 22106 ).join('')} 22107 </ul> 22108 </div> 22109 ` : ''} 22110 22111 ${productAnalysis.weaknesses ? ` 22112 <div class="analysis-section"> 22113 <h6 class="weaknesses">â Kekurangan</h6> 22114 <ul> 22115 ${productAnalysis.weaknesses.map(weakness => 22116 `<li>${escapeHtml(weakness)}</li>` 22117 ).join('')} 22118 </ul> 22119 </div> 22120 ` : ''} 22121 22122 ${productAnalysis.best_for ? ` 22123 <div class="analysis-section"> 22124 <h6 class="best-for">ð¯ Cocok Untuk</h6> 22125 <p class="best-for-text">${escapeHtml(productAnalysis.best_for)}</p> 22126 </div> 22127 ` : ''} 22128 </div> 22129 `; 22130 }
22130).filter(Boolean).join(''); 22131 } 22132 22133 /** 22134 * Update recommendations 22135 */ 22136 function updateRecommendations(recommendations) { 22137 if (!recommendations) return; 22138 22139 const budgetEl = document.getElementById('budgetRecommendation'); 22140 const performanceEl = document.getElementById('performanceRecommendation'); 22141 const balancedEl = document.getElementById('balancedRecommendation'); 22142 22143 if (budgetEl && recommendations.budget_conscious) { 22144 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.budget_conscious)); 22145 budgetEl.textContent = product ? product.name : 'Tidak tersedia'; 22146 } 22147 22148 if (performanceEl && recommendations.performance_focused) { 22149 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.performance_focused)); 22150 performanceEl.textContent = product ? product.name : 'Tidak tersedia'; 22151 } 22152 22153 if (balancedEl && recommendations.balanced) { 22154 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.balanced)); 22155 balancedEl.textContent = product ? product.name : 'Tidak tersedia'; 22156 } 22157 } 22158 22159 /** 22160 * Check if products are from different categories 22161 */ 22162 async function checkProductCategoryMismatch() { 22163 if (compareList.length < 2) { 22164 return { shouldWarn: false, categories: new Set() }; 22165 } 22166 22167 // Get the root categories from each product 22168 const rootCategories = new Set(); 22169 const productCategories = []; 22170 22171 for (const product of compareList) { 22172 // Extract root category from the product's category_path if available 22173 const rootCategory = await getProductRootCategory(product); 22174 if (rootCategory) { 22175 rootCategories.add(rootCategory); 22176 productCategories.push({ 22177 product: product.name, 22178 category: rootCategory 22179 }); 22180 } 22181 } 22182 22183 // If we have more than 1 different root categories, show warning 22184 const shouldWarn = rootCategories.size > 1 && compareList.length >= 2; 22185 22186 return { 22187 shouldWarn: shouldWarn, 22188 categories: rootCategories, 22189 productCategories: productCategories 22190 }; 22191 } 22192 22193 /** 22194 * Get product root category from category_path or fallback to name detection 22195 */ 22196 async function getProductRootCategory(product) { 22197 // First try to get from category_path if available 22198 if (product.category_path) { 22199 const rootCategory = product.category_path.split('/')[0]; 22200 return rootCategory; 22201 } 22202 22203 // If no category_path, try to fetch from product detail or use name detection 22204 const detectedCategory = detectProductCategory(product.name); 22205 return detectedCategory; 22206 } 22207 22208 /** 22209 * Detect product category from name (fallback method) 22210 */ 22211 function detectProductCategory(productName) { 22212 const name = productName.toLowerCase(); 22213 22214 // Map keywords to actual root categories from the API structure 22215 const categoryMappings = { 22216 // Komputer & Laptop 22217 'Komputer & Laptop': ['laptop', 'notebook', 'macbook', 'thinkpad', 'gaming laptop', 'desktop', 'pc', 'mini pc', 'all-in-one', 'server'], 22218 22219 // Printer & Scanner 22220 'Printer & Scanner': ['printer', 'epson printer', 'canon printer', 'hp printer', 'brother printer', 'scanner', 'laser printer', 'inkjet', 'dot matrix'], 22221 22222 // Gadget 22223 'Gadget': ['smartphone', 'iphone', 'samsung phone', 'xiaomi', 'oppo', 'vivo', 'realme', 'tablet', 'ipad', 'android tablet'], 22224 22225 // Periferal 22226 'Periferal': ['monitor', 'lcd monitor', 'led monitor', 'gaming monitor', 'keyboard', 'mouse', 'gaming keyboard', 'mechanical keyboard', 'webcam'], 22227 22228 // Audio 22229 'Audio': ['headset', 'headphone', 'earphone', 'gaming headset', 'speaker', 'mikrofon', 'sound card'], 22230 22231 // Jaringan 22232 'Jaringan': ['router', 'wifi', 'access point', 'switch', 'modem', 'network'], 22233 22234 // Penyimpanan 22235 'Penyimpanan': ['ssd', 'hdd', 'nvme', 'solid state', 'hard disk', 'flash drive', 'usb drive', 'memory card'], 22236 22237 // Komponen PC 22238 'Komponen PC': ['ram', 'memory', 'ddr4', 'ddr5', 'processor', 'cpu', 'intel', 'amd', 'motherboard', 'graphics card', 'gpu', 'psu', 'power supply', 'casing'], 22239 22240 // UPS & Power 22241 'UPS & Power': ['ups', 'uninterruptible', 'stabilizer', 'power supply'], 22242 22243 // Proyektor 22244 'Proyektor': ['proyektor', 'projector', 'layar proyektor'], 22245 22246 // Security System 22247 'Security System': ['cctv', 'security camera', 'surveillance'], 22248 22249 // Aksesoris 22250 'Aksesoris': ['charger laptop', 'adaptor', 'converter', 'dock', 'hub', 'kabel', 'cable', 'power bank', 'tas laptop'], 22251 22252 // Software 22253 'Software': ['software', 'lisensi', 'license', 'antivirus'], 22254
22255 // Perlengkapan Kantor 22256 'Perlengkapan Kantor': ['fingerprint', 'finger print', 'atk', 'alat tulis'] 22257 }; 22258 22259 // Check which root category matches 22260 for (const [rootCategory, keywords] of Object.entries(categoryMappings)) { 22261 for (const keyword of keywords) { 22262 if (name.includes(keyword)) { 22263 return rootCategory; 22264 } 22265 } 22266 } 22267 22268 return null; 22269 } 22270 22271 /** 22272 * Update buying tips 22273 */ 22274 function updateBuyingTips(tips) { 22275 const container = document.getElementById('buyingTips'); 22276 if (!container || !Array.isArray(tips)) return; 22277 22278 container.innerHTML = tips.map(tip => 22279 `<li>${escapeHtml(tip)}</li>` 22280 ).join(''); 22281 } 22282 22283 // Removed share and cart functions - replaced with AI disclaimer in UI 22284 22285 /** 22286 * Remove product from compare (public method) 22287 */ 22288 async function removeProduct(productId) { 22289 try { 22290 await removeFromCompare(productId); 22291 showToast('Produk dihapus dari perbandingan', 'success'); 22292 22293 // Update all compare buttons 22294 updateAllCompareButtons(); 22295 22296 } catch (error) { 22297 showToast(error.message || 'Terjadi kesalahan', 'error'); 22298 } 22299 } 22300 22301 /** 22302 * Utility functions 22303 */ 22304 function escapeHtml(text) { 22305 const div = document.createElement('div'); 22306 div.textContent = text; 22307 return div.innerHTML; 22308 } 22309 22310 function formatPrice(price) { 22311 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 22312 } 22313 22314 /** 22315 * Show confirmation modal using the existing modal.css design 22316 */ 22317 function showConfirmModal(options = {}) { 22318 return new Promise((resolve) => { 22319 const { 22320 title = 'Konfirmasi', 22321 message = 'Apakah Anda yakin?', 22322 confirmText = 'Ya', 22323 cancelText = 'Tidak', 22324 type = 'warning' 22325 } = options; 22326 22327 // Create modal HTML 22328 const modalHTML = ` 22329 <div class="modal-overlay" id="confirmModal"> 22330 <div class="modal-dialog"> 22331 <div class="modal-header"> 22332 <h3 class="modal-title"> 22333 <div class="modal-icon ${type}"> 22334 ${getModalIcon(type)} 22335 </div> 22336 ${escapeHtml(title)} 22337 </h3> 22338 </div> 22339 <div class="modal-body"> 22340 <p class="modal-message">${escapeHtml(message)}</p> 22341 <div class="modal-actions"> 22342 <button type="button" class="modal-btn modal-btn-secondary" id="confirmCancel"> 22343 ${escapeHtml(cancelText)} 22344 </button> 22345 <button type="button" class="modal-btn modal-btn-${type === 'danger' ? 'danger' : 'primary'}" id="confirmOk"> 22346 ${escapeHtml(confirmText)} 22347 </button> 22348 </div> 22349 </div> 22350 </div> 22351 </div> 22352 `; 22353 22354 // Add to DOM 22355 document.body.insertAdjacentHTML('beforeend', modalHTML); 22356 const modal = document.getElementById('confirmModal'); 22357 const cancelBtn = document.getElementById('confirmCancel'); 22358 const okBtn = document.getElementById('confirmOk'); 22359 22360 // Show modal 22361 requestAnimationFrame(() => { 22362 modal.classList.add('show'); 22363 }); 22364 22365 // Handle events 22366 function cleanup() { 22367 modal.remove(); 22368 } 22369 22370 function handleCancel() { 22371 modal.classList.remove('show'); 22372 setTimeout(() => { 22373 cleanup(); 22374 resolve(false); 22375 }, 200); 22376 } 22377 22378 function handleConfirm() { 22379 modal.classList.remove('show'); 22380 setTimeout(() => { 22381 cleanup(); 22382 resolve(true); 22383 }, 200); 22384 } 22385 22386 cancelBtn.addEventListener('click', handleCancel); 22387 okBtn.addEventListener('click', handleConfirm); 22388 22389 // Close on overlay click 22390 modal.addEventListener('click', (e) => { 22391 if (e.target === modal) { 22392 handleCancel(); 22393 } 22394 }); 22395 22396 // Close on escape 22397 document.addEventListener('keydown', function escapeHandler(e) { 22398 if (e.key === 'Escape') { 22399 document.removeEventListener('keydown', escapeHandler); 22400 handleCancel(); 22401 } 22402 }); 22403 22404 // Focus on confirm button 22405 setTimeout(() => okBtn.focus(), 100); 22406 }); 22407 } 22408 22409 /** 22410 * Get icon for modal type 22411 */ 22412 function getModalIcon(type) { 22413 const icons = { 22414 danger: 'ð¨ ', 22415 warning: 'â ï¸', 22416 success: 'â ', 22417 info: 'â¹ï¸' 22418 }; 22419 return icons[type] || icons.info; 22420 } 22421 22422 22423 function showToast(message, type = 'info') { 22424 // Integration with existing toast system or create simple one 22425 22426 // Simple alert fallback - replace with proper toast system 22427 if (type === 'error') { 22428 alert('Error: ' + message); 22429 } 22430 } 22431 22432 // Inline compare preview functions removed - keeping it simple and clean 22433 22434 /** 22435 * Get current product ID from page 22436 */ 22437 function getCurrentProductId() { 22438 const button = document.getElementById('pd-compare-btn'); 22439 return button ? parseInt(button.dataset.productId) : null; 22440 } 22441 22442 // hideStickyCompareBar function removed - no longer using sticky bar 22443 22444 // Public API
22445 window.productCompare = { 22446 removeProduct: removeProduct, 22447 addToCompare: addToCompare, 22448 loadCompareList: loadCompareList, 22449 getCompareList: function() { return compareList; }, 22450 openSidebar: openCompareSidebar, 22451 startAnalysis: startAIAnalysis, 22452 // Helper function to add from external sources with variant support 22453 addProductWithVariant: async function(productId, variantId = 0) { 22454 return await addToCompare(productId, variantId); 22455 } 22456 }; 22457 22458})(); 22459 22460 22461 22462</script>
vendor: 1 bytes, line 22462
22462
22463<script data-inlined="product-card-actions.js"> 22464/** 22465 * Product Card Quick Actions 22466 * Bottom sheet menu for quick add to cart & compare 22467 * Integrates with existing product-compare.js and cart.js 22468 */ 22469 22470(function() { 22471 'use strict'; 22472 22473 let activeSheet = null; 22474 let activeProductData = null; 22475 22476 // DOM elements (will be created dynamically) 22477 let overlay = null; 22478 let bottomSheet = null; 22479 22480 /** 22481 * Initialize on DOM ready 22482 */ 22483 document.addEventListener('DOMContentLoaded', function() { 22484 createBottomSheet(); 22485 attachEventListeners(); 22486 }); 22487 22488 /** 22489 * Create bottom sheet HTML structure 22490 */ 22491 function createBottomSheet() { 22492 // Create overlay 22493 overlay = document.createElement('div'); 22494 overlay.className = 'pca-overlay'; 22495 overlay.addEventListener('click', closeBottomSheet); 22496 22497 // Create bottom sheet 22498 bottomSheet = document.createElement('div'); 22499 bottomSheet.className = 'pca-bottom-sheet'; 22500 bottomSheet.innerHTML = ` 22501 <div class="pca-sheet-handle"></div> 22502 <div class="pca-sheet-product" id="pca-product-preview"> 22503 <!-- Product info will be inserted here --> 22504 </div> 22505 <div class="pca-sheet-actions"> 22506 <button type="button" class="pca-action-item cart" id="pca-add-cart"> 22507 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22508 <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"/> 22509 </svg> 22510 <div class="pca-action-content"> 22511 <div class="pca-action-label">Tambah ke Keranjang</div> 22512 <div class="pca-action-desc">Lanjut belanja produk lain</div> 22513 </div> 22514 </button> 22515 22516 <button type="button" class="pca-action-item compare" id="pca-add-compare"> 22517 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22518 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 22519 </svg> 22520 <div class="pca-action-content"> 22521 <div class="pca-action-label">Bandingkan Produk</div> 22522 <div class="pca-action-desc">Analisis dengan AI</div> 22523 </div> 22524 <span class="pca-action-badge compare" id="pca-compare-count" style="display:none;">0</span> 22525 </button> 22526 </div> 22527 `; 22528 22529 // Append to body 22530 document.body.appendChild(overlay); 22531 document.body.appendChild(bottomSheet); 22532 22533 // Setup action buttons 22534 document.getElementById('pca-add-cart').addEventListener('click', handleAddToCart); 22535 document.getElementById('pca-add-compare').addEventListener('click', handleAddToCompare); 22536 22537 // Prevent clicks inside bottom sheet from closing it 22538 bottomSheet.addEventListener('click', function(e) { 22539 e.stopPropagation(); 22540 }); 22541 } 22542 22543 /** 22544 * Attach event listeners to all 3-dot buttons 22545 */ 22546 function attachEventListeners() { 22547 // Use event delegation for dynamically loaded product cards 22548 document.addEventListener('click', function(e) { 22549 const btn = e.target.closest('.prod-card-actions-btn'); 22550 if (btn) { 22551 e.preventDefault(); 22552 e.stopPropagation(); 22553 handleActionsButtonClick(btn); 22554 } 22555 }); 22556 22557 // Handle ESC key 22558 document.addEventListener('keydown', function(e) { 22559 if (e.key === 'Escape' && activeSheet) { 22560 closeBottomSheet(); 22561 } 22562 }); 22563 } 22564 22565 /** 22566 * Handle 3-dot button click 22567 */ 22568 function handleActionsButtonClick(button) { 22569 // Get product ID directly from button 22570 const productId = parseInt(button.dataset.productId) || 0; 22571 if (!productId) { 22572 console.error('Product ID not found on button:', button); 22573 showToast('Data produk tidak ditemukan', 'error'); 22574 return; 22575 } 22576 22577 // Find parent card for additional data 22578 const card = button.closest('.prod-card, .pa-card'); 22579 if (!card) { 22580 console.error('Product card not found'); 22581 showToast('Data produk tidak ditemukan', 'error'); 22582 return; 22583 } 22584 22585 // Extract product data from card 22586 const productData = extractProductData(card, productId); 22587 if (!productData) { 22588 showToast('Data produk tidak ditemukan', 'error'); 22589 return; 22590 } 22591 22592 activeProductData = productData; 22593 openBottomSheet(productData); 22594 } 22595 22596 /** 22597 * Extract product data from card element 22598 */ 22599 function extractProductData(card, productId) { 22600 try { 22601 // Product ID already passed from button 22602 if (!productId) { 22603 console.error('Product ID not provided'); 22604 return null; 22605 } 22606 // Product ID already passed from button 22607 if (!productId) { 22608 console.error('Product ID not provided'); 22609 return null; 22610 } 22611 22612 const id = productId; 22613 22614 // Get product name 22615 const nameEl = card.querySelector('.prod-name, .title'); 22616 const name = nameEl ? nameEl.textContent.trim() : 'Produk'; 22617 22618 // Get product image 22619 const imgEl = card.querySelector('.thumb img, img'); 22620 const image = imgEl ? imgEl.src : '/assets/img/placeholder.png'; 22621 22622 // Get product price and detect variants 22623 const priceEl = card.querySelector('.prod-price-final, .final, .prod-price'); 22624 let price = 0; 22625 let hasVariants = false;
22626 let priceDisplay = ''; 22627 22628 if (priceEl) { 22629 // Get text content and clean it 22630 let priceText = priceEl.textContent.trim(); 22631 priceDisplay = priceText; 22632 22633 // Check if it's a range (indicates product has variants) 22634 if (priceText.includes('-')) { 22635 hasVariants = true; 22636 // Take the first price for calculation 22637 priceText = priceText.split('-')[0].trim(); 22638 } 22639 22640 // Remove all non-numeric characters 22641 priceText = priceText.replace(/[^0-9]/g, ''); 22642 price = parseInt(priceText) || 0; 22643 } 22644 22645 // Get stock status 22646 const stockDot = card.querySelector('.stock-dot'); 22647 let stockStatus = 'unknown'; 22648 let stockLabel = 'Cek stok'; 22649 22650 if (stockDot) { 22651 if (stockDot.classList.contains('ready')) { 22652 stockStatus = 'in'; 22653 stockLabel = 'Stok tersedia'; 22654 } else if (stockDot.classList.contains('low')) { 22655 stockStatus = 'low'; 22656 stockLabel = 'Stok terbatas'; 22657 } else if (stockDot.classList.contains('out')) { 22658 stockStatus = 'out'; 22659 stockLabel = 'Stok habis'; 22660 } 22661 } 22662 22663 // Get category path (if available) 22664 const categoryPath = card.dataset.category || ''; 22665 22666 // Get URL 22667 let url = '#'; 22668 if (card.tagName === 'A') { 22669 url = card.href; 22670 } else { 22671 const link = card.querySelector('a[href*="/product/"]'); 22672 if (link) url = link.href; 22673 } 22674 22675 return { 22676 id: id, 22677 name: name, 22678 image: image, 22679 price: price, 22680 priceDisplay: priceDisplay, 22681 hasVariants: hasVariants, 22682 stockStatus: stockStatus, 22683 stockLabel: stockLabel, 22684 categoryPath: categoryPath, 22685 url: url 22686 }; 22687 } catch (error) { 22688 console.error('Error extracting product data:', error); 22689 return null; 22690 } 22691 } 22692 22693 /** 22694 * Open bottom sheet 22695 */ 22696 function openBottomSheet(productData) { 22697 if (!overlay || !bottomSheet) return; 22698 22699 // Update product preview 22700 updateProductPreview(productData); 22701 22702 // Update cart button based on variant status 22703 updateCartButtonForVariant(productData); 22704 22705 // Update compare button state 22706 updateCompareButtonState(productData.id); 22707 22708 // Show overlay and sheet 22709 overlay.classList.add('active'); 22710 bottomSheet.classList.add('active'); 22711 document.body.style.overflow = 'hidden'; 22712 22713 activeSheet = true; 22714 22715 // Google Analytics 22716 if (typeof gtag === 'function') { 22717 gtag('event', 'quick_actions_open', { 22718 event_category: 'Product Card', 22719 event_label: 'Quick Actions Menu Opened', 22720 product_id: productData.id, 22721 product_name: productData.name 22722 }); 22723 } 22724 } 22725 22726 /** 22727 * Close bottom sheet 22728 */ 22729 function closeBottomSheet() { 22730 if (!overlay || !bottomSheet) return; 22731 22732 overlay.classList.remove('active'); 22733 bottomSheet.classList.remove('active'); 22734 document.body.style.overflow = ''; 22735 22736 activeSheet = false;
22737 activeProductData = null; 22738 } 22739 22740 /** 22741 * Update product preview in bottom sheet 22742 */ 22743 function updateProductPreview(productData) { 22744 const preview = document.getElementById('pca-product-preview'); 22745 if (!preview) return; 22746 22747 // Use full price display (shows range for variants) 22748 const priceDisplay = productData.priceDisplay || formatPrice(productData.price); 22749 22750 preview.innerHTML = ` 22751 <img src="${escapeHtml(productData.image)}" 22752 alt="${escapeHtml(productData.name)}" 22753 class="pca-product-image" 22754 loading="lazy"> 22755 <div class="pca-product-info"> 22756 <h4 class="pca-product-name">${escapeHtml(productData.name)}</h4> 22757 <p class="pca-product-price">${escapeHtml(priceDisplay)}</p> 22758 <p class="pca-product-stock ${productData.stockStatus}">${productData.stockLabel}</p> 22759 </div> 22760 `; 22761 } 22762 22763 /** 22764 * Update cart button for variant products 22765 */ 22766 function updateCartButtonForVariant(productData) { 22767 const cartBtn = document.getElementById('pca-add-cart'); 22768 if (!cartBtn) return; 22769 22770 if (productData.hasVariants) { 22771 // Product has variants - change to "View Detail" 22772 cartBtn.dataset.hasVariants = 'true'; 22773 cartBtn.dataset.productUrl = productData.url; 22774 22775 // Update icon to eye/external 22776 const iconSvg = cartBtn.querySelector('svg'); 22777 if (iconSvg) { 22778 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"/>'; 22779 } 22780 22781 // Update text 22782 const label = cartBtn.querySelector('.pca-action-label'); 22783 const desc = cartBtn.querySelector('.pca-action-desc'); 22784 if (label) label.textContent = 'Lihat Detail Produk'; 22785 if (desc) desc.textContent = 'Pilih varian terlebih dahulu'; 22786 22787 } else { 22788 // Single product - normal cart button 22789 cartBtn.dataset.hasVariants = 'false'; 22790 22791 // Restore cart icon 22792 const iconSvg = cartBtn.querySelector('svg'); 22793 if (iconSvg) { 22794 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"/>'; 22795 } 22796 22797 // Restore text 22798 const label = cartBtn.querySelector('.pca-action-label'); 22799 const desc = cartBtn.querySelector('.pca-action-desc'); 22800 if (label) label.textContent = 'Tambah ke Keranjang'; 22801 if (desc) desc.textContent = 'Lanjut belanja produk lain'; 22802 } 22803 } 22804 22805 /** 22806 * Update compare button state 22807 */ 22808 function updateCompareButtonState(productId) { 22809 const compareBtn = document.getElementById('pca-add-compare'); 22810 const compareBadge = document.getElementById('pca-compare-count'); 22811 22812 if (!compareBtn) return; 22813 22814 // Check if product is in compare list (using global compareList if available) 22815 let isInCompare = false; 22816 let compareCount = 0; 22817 22818 // Try to get compare list from product-compare.js via window.productCompare 22819 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.getCompareList === 'function') { 22820 const compareList = window.productCompare.getCompareList(); 22821 compareCount = compareList.length; 22822 isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 22823 } else { 22824 // Fallback: check badge count 22825 const mainBadge = document.getElementById('compareCount'); 22826 if (mainBadge) { 22827 compareCount = parseInt(mainBadge.textContent) || 0; 22828 } 22829 } 22830 22831 // Update button state 22832 if (isInCompare) { 22833 compareBtn.classList.add('active'); 22834 compareBtn.querySelector('.pca-action-label').textContent = 'Hapus dari Perbandingan'; 22835 compareBtn.querySelector('.pca-action-desc').textContent = 'Sudah ada di daftar'; 22836 } else { 22837 compareBtn.classList.remove('active'); 22838 compareBtn.querySelector('.pca-action-label').textContent = 'Bandingkan Produk'; 22839 compareBtn.querySelector('.pca-action-desc').textContent = 'Analisis dengan AI'; 22840 } 22841 22842 // Update badge 22843 if (compareBadge && compareCount > 0) { 22844 compareBadge.textContent = compareCount; 22845 compareBadge.style.display = 'inline-flex'; 22846 } else if (compareBadge) { 22847 compareBadge.style.display = 'none'; 22848 } 22849 } 22850 22851 /** 22852 * Handle add to cart 22853 */ 22854 async function handleAddToCart() { 22855 if (!activeProductData) return; 22856 22857 const button = document.getElementById('pca-add-cart'); 22858 if (!button) return; 22859 22860 // If product has variants, redirect to detail page 22861 if (activeProductData.hasVariants) { 22862 window.location.href = activeProductData.url; 22863 return; 22864 } 22865
22866 // Check stock 22867 if (activeProductData.stockStatus === 'out') { 22868 showToast('Produk sedang habis', 'warning'); 22869 return; 22870 } 22871 22872 // Show loading state 22873 button.classList.add('loading'); 22874 button.disabled = true; 22875 22876 // CSRF Token Helper 22877 function getCsrfToken() { 22878 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 22879 } 22880 22881 try { 22882 // Call cart API with proper format 22883 const response = await fetch('/api/cart/add.php', { 22884 method: 'POST', 22885 headers: { 22886 'Content-Type': 'application/json', 22887 'X-CSRF-Token': getCsrfToken() 22888 }, 22889 body: JSON.stringify({ 22890 csrf_token: getCsrfToken(), 22891 product_id: activeProductData.id, 22892 qty: 1, // Use 'qty' not 'quantity' 22893 variant_id: 0, // Default variant 22894 meta: { 22895 name: activeProductData.name, 22896 image_url: activeProductData.image 22897 } 22898 }), 22899 credentials: 'same-origin' 22900 }); 22901 22902 const result = await response.json(); 22903 22904 // Check both response status and result.ok 22905 if (!response.ok || !result.ok) { 22906 throw new Error(result.error || result.message || 'Gagal menambahkan ke keranjang'); 22907 } 22908 22909 // Success animation 22910 button.classList.remove('loading'); 22911 button.classList.add('success'); 22912 22913 // Update cart badge 22914 updateCartBadge(); 22915 22916 // PlazaAnalytics tracking 22917 if (window.PlazaAnalytics) { 22918 const _slug = (activeProductData.url || '').replace(/.*\/product\//, ''); 22919 window.PlazaAnalytics.track('add_to_cart', { slug: _slug }); 22920 } 22921 22922 // Show success toast 22923 showToast('Produk ditambahkan ke keranjang', 'success'); 22924 22925 // Google Analytics 22926 if (typeof gtag === 'function') { 22927 gtag('event', 'add_to_cart', { 22928 event_category: 'Ecommerce', 22929 event_label: 'Quick Add to Cart', 22930 items: [{ 22931 item_id: activeProductData.id, 22932 item_name: activeProductData.name, 22933 price: activeProductData.price, 22934 quantity: 1 22935 }] 22936 }); 22937 } 22938 22939 // Close sheet after short delay 22940 setTimeout(() => { 22941 closeBottomSheet(); 22942 button.classList.remove('success'); 22943 button.disabled = false; 22944 }, 1000); 22945 22946 } catch (error) { 22947 console.error('Add to cart error:', error); 22948 button.classList.remove('loading'); 22949 button.disabled = false; 22950 showToast(error.message || 'Terjadi kesalahan', 'error'); 22951 } 22952 } 22953 22954 /** 22955 * Handle add to compare 22956 */ 22957 async function handleAddToCompare() { 22958 if (!activeProductData) return; 22959 22960 const button = document.getElementById('pca-add-compare'); 22961 if (!button) return; 22962 22963 // Check if already in compare 22964 const isActive = button.classList.contains('active'); 22965 22966 // Check 5-product limit before adding 22967 if (!isActive) { 22968 const mainBadge = document.getElementById('compareCount'); 22969 const currentCount = mainBadge ? parseInt(mainBadge.textContent) || 0 : 0; 22970 22971 if (currentCount >= 5) { 22972 showToast('Maksimal 5 produk untuk perbandingan', 'warning'); 22973 return; 22974 } 22975 } 22976 22977 // Show loading state 22978 button.classList.add('loading'); 22979 button.disabled = true; 22980 22981 try { 22982 if (isActive) { 22983 // Remove from compare 22984 await removeFromCompare(activeProductData.id); 22985 showToast('Produk dihapus dari perbandingan', 'success'); 22986 } else { 22987 // Add to compare 22988 await addToCompare(activeProductData.id); 22989 showToast('Produk ditambahkan ke perbandingan', 'success'); 22990 22991 // Trigger fly animation 22992 triggerFlyToCompareAnimation(); 22993 } 22994 22995 // Success animation 22996 button.classList.remove('loading'); 22997 button.classList.add('success'); 22998 22999 // Update compare button state 23000 updateCompareButtonState(activeProductData.id); 23001 23002 // Close sheet after short delay 23003 setTimeout(() => { 23004 closeBottomSheet(); 23005 button.classList.remove('success'); 23006 button.disabled = false;
23007 }, 800); 23008 23009 } catch (error) { 23010 console.error('Compare error:', error); 23011 button.classList.remove('loading'); 23012 button.disabled = false; 23013 showToast(error.message || 'Terjadi kesalahan', 'error'); 23014 } 23015 } 23016 23017 /** 23018 * Add to compare (calls existing API) 23019 */ 23020 async function addToCompare(productId) { 23021 const response = await fetch('/api/compare/add.php', { 23022 method: 'POST', 23023 headers: { 23024 'Content-Type': 'application/json', 23025 }, 23026 body: JSON.stringify({ 23027 product_id: productId, 23028 variant_id: 0 23029 }), 23030 credentials: 'same-origin' 23031 }); 23032 23033 const result = await response.json(); 23034 23035 if (!result.success) { 23036 throw new Error(result.error || 'Gagal menambahkan produk'); 23037 } 23038 23039 // Trigger compare list reload if function exists 23040 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23041 await window.productCompare.loadCompareList(); 23042 } 23043 23044 return result; 23045 } 23046 23047 /** 23048 * Remove from compare (calls existing API) 23049 */ 23050 async function removeFromCompare(productId) { 23051 const response = await fetch('/api/compare/remove.php', { 23052 method: 'POST', 23053 headers: { 23054 'Content-Type': 'application/json', 23055 }, 23056 body: JSON.stringify({ 23057 product_id: productId 23058 }), 23059 credentials: 'same-origin' 23060 }); 23061 23062 const result = await response.json(); 23063 23064 if (!result.success) { 23065 throw new Error(result.error || 'Gagal menghapus produk'); 23066 } 23067 23068 // Trigger compare list reload if function exists 23069 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23070 await window.productCompare.loadCompareList(); 23071 } 23072 23073 return result; 23074 } 23075 23076 /** 23077 * Update cart badge 23078 */ 23079 function updateCartBadge() { 23080 // Try to call global cart update function if exists 23081 if (typeof window.updateCartBadge === 'function') { 23082 window.updateCartBadge(); 23083 } else { 23084 // Fallback: increment badge manually 23085 const badge = document.getElementById('cartCount'); 23086 if (badge) { 23087 const currentCount = parseInt(badge.textContent) || 0; 23088 badge.textContent = currentCount + 1; 23089 badge.classList.add('active'); 23090 } 23091 } 23092 } 23093 23094 /** 23095 * Trigger fly to compare animation 23096 */ 23097 function triggerFlyToCompareAnimation() { 23098 const compareBtn = document.getElementById('compareBtn'); 23099 if (!compareBtn || !activeProductData) return; 23100 23101 // Create flying element 23102 const flyingElement = document.createElement('div'); 23103 flyingElement.className = 'flying-product'; 23104 flyingElement.style.cssText = ` 23105 position: fixed; 23106 width: 60px; 23107 height: 60px; 23108 z-index: 99999; 23109 pointer-events: none; 23110 `; 23111 23112 flyingElement.innerHTML = ` 23113 <img src="${activeProductData.image}" 23114 style="width:100%;height:100%;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);"> 23115 `; 23116 23117 // Position at bottom sheet 23118 const sheetRect = bottomSheet.getBoundingClientRect(); 23119 const targetRect = compareBtn.getBoundingClientRect(); 23120 23121 flyingElement.style.top = `${sheetRect.top + 20}px`; 23122 flyingElement.style.left = `${sheetRect.left + 20}px`; 23123 23124 document.body.appendChild(flyingElement); 23125 23126 // Animate to target 23127 requestAnimationFrame(() => { 23128 flyingElement.style.transition = 'all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; 23129 flyingElement.style.top = `${targetRect.top + 10}px`; 23130 flyingElement.style.left = `${targetRect.left + 10}px`; 23131 flyingElement.style.transform = 'scale(0.3)'; 23132 flyingElement.style.opacity = '0'; 23133 }); 23134 23135 // Clean up 23136 setTimeout(() => { 23137 if (flyingElement.parentNode) { 23138 flyingElement.parentNode.removeChild(flyingElement); 23139 } 23140 }, 800); 23141 } 23142 23143 /** 23144 * Format price as Rupiah 23145 */ 23146 function formatPrice(price) { 23147 if (!price || price === 0) return 'Rp 0'; 23148 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 23149 } 23150 23151 /** 23152 * Escape HTML 23153 */ 23154 function escapeHtml(text) { 23155 const div = document.createElement('div'); 23156 div.textContent = text; 23157 return div.innerHTML; 23158 } 23159 23160 /** 23161 * Show toast notification (uses global showToast if available) 23162 */ 23163 function showToast(message, type = 'info') { 23164 if (typeof window.showToast === 'function') { 23165 window.showToast({ 23166 title: type === 'success' ? 'Berhasil' : type === 'error' ? 'Gagal' : 'Info', 23167 message: message, 23168 type: type, 23169 duration: 3000 23170 }); 23171 } else { 23172 // Fallback: console log 23173 console.log(`[${type.toUpperCase()}] ${message}`); 23174 } 23175 } 23176 23177 /** 23178 * Expose functions for external access 23179 */ 23180 window.ProductCardActions = {
23181 openSheet: openBottomSheet, 23182 closeSheet: closeBottomSheet, 23183 updateCompareState: updateCompareButtonState 23184 }; 23185 23186})(); 23187 23188</script>
23188 23189
23189<script data-inlined="product-reviews.js"> 23190/** 23191 * Product Reviews - Frontend JavaScript 23192 * Handles review display, submission, and helpful voting 23193 */ 23194 23195const ProductReviews = (function() { 23196 'use strict'; 23197 23198 let currentPage = 1; 23199 let currentSort = 'helpful'; 23200 let productId = null; 23201 let isLoading = false; 23202 23203 /** 23204 * Public API: Initialize reviews for a product 23205 */ 23206 function init(pid) { 23207 productId = pid; 23208 23209 if (!productId) { 23210 console.warn('Product ID not provided to ProductReviews.init()'); 23211 return; 23212 } 23213 23214 initializeReviewTab(); 23215 loadReviews(); 23216 } 23217 23218 /** 23219 * Initialize review tab functionality 23220 */ 23221 function initializeReviewTab() { 23222 // Tab switching 23223 const tabButtons = document.querySelectorAll('.tab-btn'); 23224 tabButtons.forEach(btn => { 23225 btn.addEventListener('click', function() { 23226 const tabName = this.dataset.tab; 23227 switchTab(tabName); 23228 }); 23229 }); 23230 23231 // Sort dropdown 23232 const sortSelect = document.getElementById('review-sort'); 23233 if (sortSelect) { 23234 sortSelect.addEventListener('change', function() { 23235 currentSort = this.value; 23236 currentPage = 1; 23237 loadReviews(); 23238 }); 23239 } 23240 23241 // Load more button
23242 const loadMoreBtn = document.getElementById('load-more-reviews'); 23243 if (loadMoreBtn) { 23244 loadMoreBtn.addEventListener('click', function() { 23245 currentPage++; 23246 loadReviews(true); // Append mode 23247 }); 23248 } 23249 23250 // Write review button 23251 const writeReviewBtn = document.getElementById('write-review-btn'); 23252 if (writeReviewBtn) { 23253 writeReviewBtn.addEventListener('click', showReviewModal); 23254 } 23255 } 23256 23257 /** 23258 * Switch between product tabs 23259 */ 23260 function switchTab(tabName) { 23261 // Update button states 23262 document.querySelectorAll('.tab-btn').forEach(btn => { 23263 btn.classList.toggle('active', btn.dataset.tab === tabName); 23264 }); 23265 23266 // Update content visibility 23267 document.querySelectorAll('.tab-content').forEach(content => { 23268 content.style.display = content.id === `${tabName}-tab` ? 'block' : 'none'; 23269 }); 23270 } 23271 23272 /** 23273 * Load reviews from API 23274 */ 23275 async function loadReviews(append = false) { 23276 if (isLoading) return; 23277 isLoading = true; 23278 23279 const container = document.getElementById('reviews-list');
23280 const loadMoreBtn = document.getElementById('load-more-reviews'); 23281 23282 // Check if review section exists (product might not have reviews section) 23283 if (!container) { 23284 console.log('Review section not found - product may not have reviews yet'); 23285 isLoading = false; 23286 return; 23287 } 23288 23289 try { 23290 if (!append) { 23291 container.innerHTML = '<div class="review-loading">Memuat ulasan...</div>'; 23292 } else { 23293 loadMoreBtn.disabled = true; 23294 loadMoreBtn.textContent = 'Memuat...'; 23295 } 23296 23297 const response = await fetch(`/api/reviews/list.php?product_id=${productId}&sort=${currentSort}&page=${currentPage}&limit=10`); 23298 const data = await response.json(); 23299 23300 if (!data.success) { 23301 throw new Error(data.message || 'Gagal memuat ulasan'); 23302 } 23303 23304 // Update statistics (only on initial load) 23305 if (!append && data.stats) { 23306 updateReviewStats(data.stats); 23307 } 23308 23309 // Render reviews 23310 if (!append) { 23311 container.innerHTML = ''; 23312 } 23313 23314 const reviews = data.reviews || []; 23315 23316 if (reviews.length === 0 && !append) { 23317 container.innerHTML = '<div class="no-reviews">Belum ada ulasan untuk produk ini. Jadilah yang pertama!</div>'; 23318 } else { 23319 reviews.forEach(review => { 23320 container.appendChild(createReviewElement(review)); 23321 }); 23322 } 23323 23324 // Update load more button 23325 if (loadMoreBtn) { 23326 if (data.pagination.has_more) { 23327 loadMoreBtn.style.display = 'block'; 23328 loadMoreBtn.disabled = false; 23329 loadMoreBtn.textContent = 'Muat Lebih Banyak'; 23330 } else { 23331 loadMoreBtn.style.display = 'none'; 23332 } 23333 } 23334 23335 } catch (error) { 23336 console.error('Load reviews error:', error); 23337 container.innerHTML = '<div class="review-error">Gagal memuat ulasan. Silakan coba lagi.</div>'; 23338 } finally { 23339 isLoading = false; 23340 } 23341 } 23342 23343 /** 23344 * Update review statistics display 23345 */ 23346 function updateReviewStats(stats) { 23347 const avgRatingEl = document.getElementById('avg-rating'); 23348 const totalReviewsEl = document.getElementById('total-reviews'); 23349 const ratingBarsContainer = document.getElementById('rating-bars'); 23350 23351 if (avgRatingEl) { 23352 avgRatingEl.textContent = stats.average_rating.toFixed(1); 23353 } 23354 23355 if (totalReviewsEl) { 23356 totalReviewsEl.textContent = `${stats.total_reviews} ulasan`; 23357 } 23358 23359 // Update rating breakdown bars 23360 if (ratingBarsContainer) { 23361 ratingBarsContainer.innerHTML = ''; 23362 stats.rating_breakdown.forEach(item => { 23363 const bar = document.createElement('div'); 23364 bar.className = 'rating-bar'; 23365 bar.innerHTML = ` 23366 <span class="rating-label">${item.rating}â </span> 23367 <div class="bar-container"> 23368 <div class="bar-fill" style="width: ${item.percentage}%"></div> 23369 </div> 23370 <span class="rating-count">${item.count}</span> 23371 `; 23372 ratingBarsContainer.appendChild(bar); 23373 }); 23374 } 23375 23376 // Update tab count 23377 const reviewTab = document.querySelector('[data-tab="reviews"]'); 23378 if (reviewTab && stats.total_reviews > 0) { 23379 reviewTab.textContent = `Ulasan (${stats.total_reviews})`; 23380 } 23381 } 23382 23383 /** 23384 * Create review element 23385 */ 23386 function createReviewElement(review) { 23387 const div = document.createElement('div'); 23388 div.className = 'review-item'; 23389 div.dataset.reviewId = review.ID; 23390 23391 // Build stars 23392 const stars = 'â '.repeat(review.RATING) + 'â'.repeat(5 - review.RATING); 23393 23394 // Build images if available 23395 let imagesHtml = ''; 23396 if (review.images && review.images.length > 0) { 23397 imagesHtml = '<div class="review-images">'; 23398 review.images.forEach(img => { 23399 imagesHtml += `<img src="${escapeHtml(img)}" alt="Review image" class="review-img" loading="lazy">`; 23400 }); 23401 imagesHtml += '</div>'; 23402 } 23403 23404 // Verified purchase badge 23405 const verifiedBadge = review.IS_VERIFIED_PURCHASE === 'Y' 23406 ? '<span class="verified-badge">â Pembelian Terverifikasi</span>' 23407 : ''; 23408 23409 // Use DISPLAY_NAME if available (for anonymous reviews), otherwise use FULL_NAME 23410 const displayName = review.DISPLAY_NAME || review.FULL_NAME || 'Anonymous'; 23411 const anonymousIndicator = review.IS_ANONYMOUS === 'Y' 23412 ? '<span class="anonymous-badge" title="Nama pengguna disamark
23412an untuk privasi">ð</span>' 23413 : ''; 23414 23415 div.innerHTML = ` 23416 <div class="review-header"> 23417 <div class="review-author"> 23418 <strong>${escapeHtml(displayName)}</strong> 23419 ${anonymousIndicator} 23420 ${verifiedBadge} 23421 </div> 23422 <div class="review-rating"> 23423 <span class="stars">${stars}</span> 23424 </div> 23425 </div> 23426 <div class="review-date">${review.created_ago}</div> 23427 <div class="review-text">${escapeHtml(review.REVIEW_TEXT).replace(/\n/g, '<br>')}</div> 23428 ${imagesHtml} 23429 <div class="review-actions"> 23430 <button class="btn-helpful" data-review-id="${review.ID}"> 23431 <span class="helpful-icon">ð</span> 23432 <span class="helpful-text">Membantu (${review.HELPFUL_COUNT || 0})</span> 23433 </button> 23434 </div> 23435 `; 23436 23437 // Add helpful button listener 23438 const helpfulBtn = div.querySelector('.btn-helpful'); 23439 helpfulBtn.addEventListener('click', () => markAsHelpful(review.ID)); 23440 23441 return div; 23442 } 23443 23444 /** 23445 * Get CSRF token from meta tag 23446 */ 23447 function getCsrfToken() { 23448 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23449 return metaTag ? metaTag.getAttribute('content') : ''; 23450 } 23451 23452 /** 23453 * Mark review as helpful 23454 */ 23455 async function markAsHelpful(reviewId) { 23456 // Check if logged in 23457 const isLoggedIn = document.body.classList.contains('logged-in') || 23458 document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 23459 23460 if (!isLoggedIn) { 23461 showModal({ 23462 title: 'Login Diperlukan', 23463 message: 'Silakan login terlebih dahulu untuk memberikan vote pada ulasan ini.', 23464 icon: 'info', 23465 buttons: [ 23466 { text: 'Batal', className: 'modal-btn-secondary' }, 23467 { text: 'Login', className: 'modal-btn-primary', callback: () => window.location.href = '/login' } 23468 ] 23469 }); 23470 return; 23471 } 23472 23473 // Get current button element 23474 const reviewItem = document.querySelector(`[data-review-id="${reviewId}"]`); 23475 const helpfulBtn = reviewItem?.querySelector('.btn-helpful'); 23476 23477 if (!helpfulBtn) return; 23478 23479 // Disable button during request 23480 helpfulBtn.disabled = true; 23481 const originalText = helpfulBtn.querySelector('.helpful-text').textContent; 23482 23483 try { 23484 const response = await fetch('/api/reviews/helpful.php', { 23485 method: 'POST', 23486 headers: { 23487 'Content-Type': 'application/json', 23488 'X-CSRF-Token': getCsrfToken() 23489 }, 23490 credentials: 'same-origin', 23491 body: JSON.stringify({ 23492 csrf_token: getCsrfToken(), 23493 review_id: reviewId 23494 }) 23495 }); 23496 23497 const data = await response.json(); 23498 23499 if (data.success) { 23500 // Update button count immediately based on action 23501 const currentCount = parseInt(originalText.match(/\d+/)?.[0] || 0); 23502 const newCount = data.action === 'added' ? currentCount + 1 : currentCount - 1; 23503 helpfulBtn.querySelector('.helpful-text').textContent = `Membantu (${newCount})`; 23504 23505 // Show success message 23506 showToast(data.message, 'success'); 23507 23508 // Re-enable button 23509 helpfulBtn.disabled = false; 23510 } else { 23511 showToast(data.message, 'error'); 23512 helpfulBtn.disabled = false; 23513 } 23514 23515 } catch (error) { 23516 console.error('Mark helpful error:', error); 23517 showToast('Gagal memberikan vote. Silakan coba lagi.', 'error'); 23518 helpfulBtn.disabled = false; 23519 } 23520 } 23521 23522 /** 23523 * Show review submission modal (placeholder) 23524 */ 23525 function showReviewModal() { 23526 // TODO: Implement review modal 23527 // For now, redirect to account page 23528 window.location.href = '/account/orders'; 23529 } 23530 23531 /** 23532 * Show toast notification 23533 */ 23534 function showToast(message, type = 'info') { 23535 const toast = document.createElement('div'); 23536 toast.className = `review-toast toast-${type}`; 23537 toast.textContent = message; 23538 23539 document.body.appendChild(toast); 23540 23541 setTimeout(() => { 23542 toast.classList.add('show'); 23543 }, 100); 23544 23545 setTimeout(() => { 23546 toast.classList.remove('show'); 23547 setTimeout(() => toast.remove(), 300); 23548 }, 3000); 23549 } 23550 23551 /** 23552 * Show modal dialog 23553 * @param {Object} options - Modal options 23554 * @param {string} options.title - Modal title 23555 * @param {string} options.message - Modal message 23556 * @param {string} options.icon - Icon type (info, success, warning, danger) 23557 * @param {Array} options.buttons - Array of button objects {text, className, callback} 23558 */ 23559 function showModal(options) { 23560 const {
23561 title = 'Konfirmasi', 23562 message = '', 23563 icon = 'info', 23564 buttons = [{ text: 'OK', className: 'modal-btn-primary' }] 23565 } = options; 23566 23567 // Icon emoji mapping 23568 const iconEmojis = { 23569 info: 'â¹ï¸', 23570 success: 'â', 23571 warning: 'â ï¸', 23572 danger: 'â' 23573 }; 23574 23575 // Create modal HTML 23576 const modalHTML = ` 23577 <div class="modal-overlay" id="helpful-vote-modal" style="display: flex !important; align-items: center !important; justify-content: center !important;"> 23578 <div class="modal-dialog"> 23579 <div class="modal-header"> 23580 <h3 class="modal-title"> 23581 <span class="modal-icon ${icon}">${iconEmojis[icon] || iconEmojis.info}</span> 23582 ${escapeHtml(title)} 23583 </h3> 23584 </div> 23585 <div class="modal-body"> 23586 <p class="modal-message">${escapeHtml(message)}</p> 23587 <div class="modal-actions"> 23588 ${buttons.map((btn, index) => 23589 `<button class="modal-btn ${btn.className || 'modal-btn-secondary'}" data-index="${index}">${escapeHtml(btn.text)}</button>` 23590 ).join('')} 23591 </div> 23592 </div> 23593 </div> 23594 </div> 23595 `; 23596 23597 // Append to body 23598 const tempDiv = document.createElement('div'); 23599 tempDiv.innerHTML = modalHTML; 23600 const modal = tempDiv.firstElementChild; 23601 document.body.appendChild(modal); 23602 23603 // Add event listeners to buttons 23604 buttons.forEach((btn, index) => { 23605 const btnElement = modal.querySelector(`[data-index="${index}"]`); 23606 btnElement.addEventListener('click', () => { 23607 closeModal(modal); 23608 if (btn.callback && typeof btn.callback === 'function') { 23609 btn.callback(); 23610 } 23611 }); 23612 }); 23613 23614 // Close on overlay click 23615 modal.addEventListener('click', (e) => { 23616 if (e.target === modal) { 23617 closeModal(modal); 23618 } 23619 }); 23620 23621 // Show modal with animation 23622 setTimeout(() => modal.classList.add('show'), 10); 23623 } 23624 23625 /** 23626 * Close modal dialog 23627 */ 23628 function closeModal(modal) { 23629 modal.classList.remove('show'); 23630 setTimeout(() => modal.remove(), 200); 23631 } 23632 23633 /** 23634 * Escape HTML for XSS protection 23635 */ 23636 function escapeHtml(text) { 23637 const div = document.createElement('div'); 23638 div.textContent = text; 23639 return div.innerHTML; 23640 } 23641 23642 /** 23643 * Get product ID from URL 23644 */ 23645 function getProductIdFromUrl() { 23646 const match = window.location.pathname.match(/\/product\/(\d+)/); 23647 return match ? match[1] : null; 23648 } 23649 23650 // Public API 23651 return { 23652 init: init 23653 }; 23654 23655})(); 23656 23657</script>
23657 23658
23658<script data-inlined="review-submission.js"> 23659/** 23660 * Review Submission Modal & Handler 23661 * Handles review form display, validation, and submission 23662 */ 23663 23664(function() { 23665 'use strict'; 23666 23667 let currentProductId = null; 23668 let currentOrderId = null; 23669 let currentSoRefId = null; 23670 let selectedRating = 0; 23671 let uploadedImages = []; 23672 23673 // Get CSRF token from meta tag 23674 function getCsrfToken() { 23675 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23676 return metaTag ? metaTag.getAttribute('content') : ''; 23677 } 23678 23679 // Initialize 23680 function init() { 23681 createReviewModal(); 23682 attachEventListeners(); 23683 loadReviewableOrders(); 23684 } 23685 23686 // Create review modal HTML 23687 function createReviewModal() { 23688 const modalHTML = ` 23689 <div class="modal-overlay" id="review-modal-overlay" style="display:none;"> 23690 <div class="modal-content review-modal"> 23691 <div class="modal-header"> 23692 <h3 class="modal-title">Tulis Ulasan</h3> 23693 <button type="button" class="modal-close" id="review-modal-close" aria-label="Tutup"> 23694 <svg width="20" height="20" viewBox="0 0 24 24"> 23695 <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"/> 23696 </svg> 23697 </button> 23698 </div> 23699 23700 <div class="modal-body"> 23701 <form id="review-form" class="review-form"> 23702 <!-- Product Info --> 23703 <div class="review-product-info" id="review-product-info"> 23704 <img id="review-product-image" src="" alt="" class="review-product-thumb"> 23705 <div class="review-product-details"> 23706 <h4 id="review-product-name"></h4> 23707 <p id="review-product-sku"></p> 23708 </div> 23709 </div> 23710 23711 <!-- Rating --> 23712 <div class="form-group"> 23713 <label class="form-label">Rating <span style="color:#ef4444;">*</span></label> 23714 <div class="star-rating-input" id="star-rating-input"> 23715 <button type="button" class="star-btn" data-rating="1">â </button> 23716 <button type="button" class="star-btn" data-rating="2">â </button> 23717 <button type="button" class="star-btn" data-rating="3">â </button> 23718 <button type="button" class="star-btn" data-rating="4">â </button> 23719 <button type="button" class="star-btn" data-rating="5">â </button> 23720 </div> 23721 <p class="rating-label" id="rating-label">Pilih rating Anda</p> 23722 </div> 23723 23724 <!-- Review Text --> 23725 <div class="form-group"> 23726 <label class="form-label" for="review-text">Ulasan Anda <span style="color:#ef4444;">*</span></label> 23727 <textarea 23728 id="review-text" 23729 name="review_text" 23730 class="form-control" 23731 rows="6" 23732 placeholder="Ceritakan pengalaman Anda dengan produk ini..." 23733 minlength="20" 23734 maxlength="5000" 23735 required></textarea> 23736 <p class="form-hint"> 23737 <span id="review-char-count">0</span>/5000 karakter 23738 <span style="color:#059669;font-weight:500;"> 23739 ð° +1,000 point untuk ulasan ⥠100 karakter 23740 </span> 23741 </p> 23742 </div> 23743 23744 <!-- Image Upload --> 23745 <div class="form-group"> 23746 <label class="form-label">Upload Foto (Opsional)</label> 23747 <div class="image-upload-container"> 23748 <input type="file" 23749 id="review-images" 23750 name="images[]" 23751 accept="image/jpeg,image/jpg,image/png,image/webp" 23752 multiple 23753 style="display:none;"> 23754 <button type="button" class="btn-upload-trigger" id="upload-trigger"> 23755 <svg width="24" height="24" viewBox="0 0 24 24">
23756 <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"/> 23757 </svg> 23758 <span>Pilih Foto</span> 23759 </button> 23760 <p class="form-hint"> 23761 Max 5 foto, format JPG/PNG/WebP, max 2MB per foto 23762 <span style="color:#059669;font-weight:500;"> 23763 ð° +2,000 point untuk ulasan dengan foto 23764 </span> 23765 </p> 23766 </div> 23767 <div class="image-preview-grid" id="image-preview-grid"></div> 23768 </div> 23769 23770 <!-- Point Reward Info --> 23771 <div class="point-reward-info"> 23772 <div class="pri-header"> 23773 <span>ð</span> 23774 <span>Dapatkan Point Reward</span> 23775 </div> 23776 <ul class="pri-list"> 23777 <li>â Ulasan dasar: <strong>5,000 point</strong></li> 23778 <li id="pri-long-text" style="opacity:0.5;">ð Ulasan panjang (â¥100 kata): <strong>+1,000 point</strong></li> 23779 <li id="pri-images" style="opacity:0.5;">ð¸ Dengan foto: <strong>+2,000 point</strong></li> 23780 </ul> 23781 <div class="pri-total"> 23782 Total Reward: <strong id="pri-total-points">5,000 point</strong> 23783 </div> 23784 </div> 23785 23786 <!-- Privacy Option --> 23787 <div class="form-group" style="margin-top:20px;padding:15px;background:#f8fafc;border-radius:8px;"> 23788 <label class="checkbox-label" style="display:flex;align-items:center;cursor:pointer;margin:0;"> 23789 <input type="checkbox" 23790 id="review-anonymous" 23791 name="is_anonymous" 23792 style="margin-right:8px;width:18px;height:18px;cursor:pointer;"> 23793 <span style="font-size:14px;"> 23794 <strong>ð Sembunyikan nama saya</strong> 23795 <span style="color:#64748b;display:block;font-size:13px;margin-top:4px;" id="review-anonymous-preview"> 23796 Nama Anda akan ditampilkan dengan disamarkan untuk menjaga privasi 23797 </span> 23798 </span> 23799 </label> 23800 </div> 23801 23802 <!-- Error Alert --> 23803 <div class="form-error" id="review-form-error" style="display:none;"></div> 23804 23805 <!-- Submit Buttons --> 23806 <div class="modal-footer"> 23807 <button type="button" class="btn btn-secondary" id="review-cancel">Batal</button> 23808 <button type="submit" class="btn btn-primary" id="review-submit"> 23809 Kirim Ulasan 23810 </button> 23811 </div> 23812 </form> 23813 </div> 23814 </div> 23815 </div> 23816 `; 23817 23818 document.body.insertAdjacentHTML('beforeend', modalHTML); 23819 } 23820 23821 // Attach event listeners 23822 function attachEventListeners() { 23823 // Modal triggers - event delegation 23824 document.addEventListener('click', function(e) { 23825 const btn = e.target.closest('.btn-write-review'); 23826 if (btn) { 23827 e.preventDefault(); 23828 openReviewModal( 23829 btn.dataset.productId, 23830 btn.dataset.orderId, 23831 btn.dataset.productName, 23832 btn.dataset.productImage, 23833 btn.dataset.productSku, 23834 btn.dataset.soRefId || null 23835 ); 23836 } 23837 }); 23838 23839 // Close modal 23840 setTimeout(() => { 23841 const closeBtn = document.getElementById('review-modal-close'); 23842 const cancelBtn = document.getElementById('review-cancel');
23843 const overlay = document.getElementById('review-modal-overlay'); 23844 23845 if (closeBtn) { 23846 closeBtn.addEventListener('click', closeReviewModal); 23847 } 23848 if (cancelBtn) { 23849 cancelBtn.addEventListener('click', closeReviewModal); 23850 } 23851 if (overlay) { 23852 overlay.addEventListener('click', function(e) { 23853 if (e.target === overlay) closeReviewModal(); 23854 }); 23855 } 23856 }, 100); 23857 23858 // Star rating 23859 setTimeout(() => { 23860 const starButtons = document.querySelectorAll('.star-btn'); 23861 starButtons.forEach(btn => { 23862 btn.addEventListener('click', function(e) { 23863 e.preventDefault(); 23864 const rating = parseInt(this.dataset.rating); 23865 setRating(rating); 23866 }); 23867 }); 23868 }, 100); 23869 23870 // Character count 23871 setTimeout(() => { 23872 const textarea = document.getElementById('review-text'); 23873 if (textarea) { 23874 textarea.addEventListener('input', function() { 23875 updateCharCount(); 23876 calculatePotentialPoints(); 23877 }); 23878 } 23879 }, 100); 23880 23881 // Image upload 23882 setTimeout(() => { 23883 const uploadTrigger = document.getElementById('upload-trigger'); 23884 const fileInput = document.getElementById('review-images'); 23885 23886 if (uploadTrigger && fileInput) { 23887 uploadTrigger.addEventListener('click', () => fileInput.click()); 23888 fileInput.addEventListener('change', handleImageSelection); 23889 } 23890 }, 100); 23891 23892 // Form submit 23893 setTimeout(() => { 23894 const form = document.getElementById('review-form'); 23895 if (form) { 23896 form.addEventListener('submit', handleSubmit); 23897 } 23898 }, 100); 23899 } 23900 23901 // Open review modal 23902 function openReviewModal(productId, orderId, productName, productImage, productSku, soRefId = null) { 23903 const overlay = document.getElementById('review-modal-overlay'); 23904 if (!overlay) { 23905 console.error('Modal overlay not found!'); 23906 return; 23907 } 23908 23909 currentProductId = productId; 23910 currentOrderId = orderId; 23911 currentSoRefId = soRefId; 23912 selectedRating = 0; 23913 uploadedImages = []; 23914 23915 // Set product info 23916 const nameEl = document.getElementById('review-product-name'); 23917 const imageEl = document.getElementById('review-product-image'); 23918 const skuEl = document.getElementById('review-product-sku'); 23919 23920 if (nameEl) nameEl.textContent = productName; 23921 if (imageEl) imageEl.src = productImage || '/assets/img/no-image.png'; 23922 if (skuEl) skuEl.textContent = 'SKU: ' + productSku; 23923 23924 // Update privacy preview with actual user name 23925 updatePrivacyPreview(); 23926 23927 // Reset form 23928 const form = document.getElementById('review-form'); 23929 const previewGrid = document.getElementById('image-preview-grid'); 23930 const errorEl = document.getElementById('review-form-error'); 23931 23932 if (form) form.reset(); 23933 if (previewGrid) previewGrid.innerHTML = ''; 23934 if (errorEl) errorEl.style.display = 'none'; 23935 23936 setRating(0); 23937 updateCharCount(); 23938 calculatePotentialPoints(); 23939 23940 // Show modal 23941 overlay.style.display = 'flex'; 23942 overlay.classList.add('show'); 23943 document.body.style.overflow = 'hidden'; 23944 } 23945 23946 // Close review modal 23947 function closeReviewModal() { 23948 const overlay = document.getElementById('review-modal-overlay'); 23949 if (overlay) { 23950 overlay.classList.remove('show'); 23951 setTimeout(() => { 23952 overlay.style.display = 'none'; 23953 }, 200); // Wait for fade animation 23954 } 23955 document.body.style.overflow = ''; 23956 } 23957 23958 // Set star rating 23959 function setRating(rating) { 23960 selectedRating = rating; 23961 const stars = document.querySelectorAll('.star-btn'); 23962 const labels = ['', 'Sangat Buruk', 'Buruk', 'Cukup', 'Bagus', 'Sangat Bagus']; 23963
23964 stars.forEach((star, index) => { 23965 if (index < rating) { 23966 star.classList.add('active'); 23967 } else { 23968 star.classList.remove('active'); 23969 } 23970 }); 23971 23972 document.getElementById('rating-label').textContent = labels[rating] || 'Pilih rating Anda'; 23973 } 23974 23975 // Update character count 23976 function updateCharCount() { 23977 const textarea = document.getElementById('review-text'); 23978 const count = textarea.value.length; 23979 document.getElementById('review-char-count').textContent = count; 23980 } 23981 23982 // Calculate potential points 23983 function calculatePotentialPoints() { 23984 const textarea = document.getElementById('review-text'); 23985 if (!textarea) return; 23986 23987 const charCount = textarea.value.trim().length; 23988 const hasImages = uploadedImages.length > 0; 23989 23990 let totalPoints = 5000; 23991 let longTextActive = charCount >= 100; 23992 let imagesActive = hasImages; 23993 23994 if (longTextActive) totalPoints += 1000; 23995 if (imagesActive) totalPoints += 2000; 23996 23997 // Update UI 23998 const longTextEl = document.getElementById('pri-long-text'); 23999 const imagesEl = document.getElementById('pri-images'); 24000 const totalEl = document.getElementById('pri-total-points'); 24001 24002 if (longTextEl) longTextEl.style.opacity = longTextActive ? '1' : '0.5'; 24003 if (imagesEl) imagesEl.style.opacity = imagesActive ? '1' : '0.5'; 24004 if (totalEl) totalEl.textContent = totalPoints.toLocaleString('id-ID') + ' point'; 24005 } 24006 24007 // Handle image selection 24008 function handleImageSelection(e) { 24009 const files = Array.from(e.target.files); 24010 24011 if (files.length > 5) { 24012 showError('Maksimal 5 foto yang dapat diupload'); 24013 e.target.value = ''; // Reset input 24014 return; 24015 } 24016 24017 uploadedImages = []; 24018 const previewGrid = document.getElementById('image-preview-grid'); 24019 previewGrid.innerHTML = ''; 24020 24021 files.forEach((file, index) => { 24022 // Validate file type 24023 if (!file.type.match(/image\/(jpeg|jpg|png|webp)/)) { 24024 showError('Format file harus JPG, PNG, atau WebP'); 24025 return; 24026 } 24027 24028 // Validate file size (2MB) 24029 if (file.size > 2 * 1024 * 1024) { 24030 showError('Ukuran file maksimal 2MB'); 24031 return; 24032 } 24033 24034 uploadedImages.push(file); 24035 24036 // Create preview 24037 const reader = new FileReader(); 24038 reader.onload = function(e) { 24039 const previewHTML = ` 24040 <div class="image-preview-item"> 24041 <img src="${e.target.result}" alt="Preview ${index + 1}"> 24042 <button type="button" class="remove-image" data-index="${index}" aria-label="Hapus"> 24043 <svg width="16" height="16" viewBox="0 0 24 24"> 24044 <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"/> 24045 </svg> 24046 </button> 24047 </div> 24048 `; 24049 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24050 }; 24051 reader.readAsDataURL(file); 24052 }); 24053 24054 calculatePotentialPoints(); 24055 24056 // Attach remove handlers 24057 setTimeout(() => { 24058 document.querySelectorAll('.remove-image').forEach(btn => { 24059 btn.addEventListener('click', function() { 24060 const index = parseInt(this.dataset.index); 24061 removeImage(index); 24062 }); 24063 }); 24064 }, 100); 24065 } 24066 24067 // Remove image 24068 function removeImage(index) { 24069 uploadedImages.splice(index, 1); 24070 24071 const previewGrid = document.getElementById('image-preview-grid'); 24072 previewGrid.innerHTML = ''; 24073 24074 uploadedImages.forEach((file, idx) => { 24075 const reader = new FileReader(); 24076 reader.onload = function(e) { 24077 const previewHTML = ` 24078 <div class="image-preview-item"> 24079 <img src="${e.target.result}" alt="Preview ${idx + 1}"> 24080 <button type="button" class="remove-image" data-index="${idx}" aria-label="Hapus">
24081 <svg width="16" height="16" viewBox="0 0 24 24"> 24082 <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"/> 24083 </svg> 24084 </button> 24085 </div> 24086 `; 24087 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24088 }; 24089 reader.readAsDataURL(file); 24090 }); 24091 24092 calculatePotentialPoints(); 24093 24094 setTimeout(() => { 24095 document.querySelectorAll('.remove-image').forEach(btn => { 24096 btn.addEventListener('click', function() { 24097 const idx = parseInt(this.dataset.index); 24098 removeImage(idx); 24099 }); 24100 }); 24101 }, 100); 24102 } 24103 24104 // Handle form submission 24105 async function handleSubmit(e) { 24106 e.preventDefault(); 24107 24108 // Validate 24109 if (selectedRating === 0) { 24110 showError('Silakan pilih rating terlebih dahulu'); 24111 return; 24112 } 24113 24114 const reviewText = document.getElementById('review-text').value.trim(); 24115 if (reviewText.length < 20) { 24116 showError('Ulasan minimal 20 karakter'); 24117 return; 24118 } 24119 24120 const submitBtn = document.getElementById('review-submit'); 24121 submitBtn.disabled = true; 24122 24123 try { 24124 // Step 1: Upload images if any 24125 let imageUrls = []; 24126 if (uploadedImages.length > 0) { 24127 submitBtn.textContent = 'Mengupload foto...'; 24128 imageUrls = await uploadImages(); 24129 if (!imageUrls) { 24130 // Upload failed 24131 submitBtn.disabled = false; 24132 submitBtn.textContent = 'Kirim Ulasan'; 24133 return; 24134 } 24135 } 24136 24137 // Step 2: Submit review with image URLs 24138 submitBtn.textContent = 'Mengirim ulasan...'; 24139 24140 const payload = { 24141 csrf_token: getCsrfToken(), 24142 product_id: currentProductId, 24143 order_id: currentOrderId || 0, 24144 so_ref_id: currentSoRefId || undefined, 24145 rating: selectedRating, 24146 review_text: reviewText, 24147 is_anonymous: document.getElementById('review-anonymous').checked, 24148 images: imageUrls 24149 }; 24150 24151 const response = await fetch('/api/reviews/submit.php', { 24152 method: 'POST', 24153 headers: { 24154 'Content-Type': 'application/json', 24155 'X-CSRF-Token': getCsrfToken() 24156 }, 24157 body: JSON.stringify(payload) 24158 }); 24159 24160 const result = await response.json(); 24161 24162 if (result.success) { 24163 showSuccess(result.message || 'Ulasan berhasil dikirim!'); 24164 closeReviewModal(); 24165 24166 // Refresh reviewable orders if on account page 24167 if (typeof loadReviewableOrders === 'function') { 24168 setTimeout(loadReviewableOrders, 500); 24169 } 24170 } else { 24171 showError(result.message || 'Gagal mengirim ulasan'); 24172 } 24173 } catch (error) { 24174 console.error('Submit error:', error); 24175 showError('Terjadi kesalahan saat mengirim ulasan'); 24176 } finally { 24177 submitBtn.disabled = false; 24178 submitBtn.textContent = 'Kirim Ulasan'; 24179 } 24180 } 24181 24182 /** 24183 * Upload images to server 24184 * @returns {Promise<Array|null>} Array of image URLs or null if failed 24185 */ 24186 async function uploadImages() { 24187 if (uploadedImages.length === 0) { 24188 return []; 24189 } 24190 24191 const formData = new FormData(); 24192 uploadedImages.forEach(file => { 24193 formData.append('images[]', file); 24194 }); 24195 24196 try { 24197 const response = await fetch('/api/reviews/upload_images.php', { 24198 method: 'POST', 24199 headers: { 24200 'X-CSRF-Token': getCsrfToken() 24201 }, 24202 body: formData 24203 }); 24204 24205 // Check if response is JSON 24206 const contentType = response.headers.get('content-type'); 24207 if (!contentType || !contentType.includes('application/json')) { 24208 const text = await response.text(); 24209 console.error('Non-JSON response:', text); 24210 showError('Server error: Response bukan JSON. Cek browser console untuk detail.'); 24211 return null; 24212 } 24213 24214 const result = await response.json(); 24215 24216 if (result.success && result.images) { 24217 // Extract URLs from uploaded images 24218 return result.images.map(img => img.url); 24219 } else { 24220 showError(result.message || 'Gagal mengupload foto'); 24221 return null; 24222 } 24223 } catch (error) { 24224 console.error('Upload images error:', error); 24225 showError('Terjadi kesalahan saat mengupload foto. Cek console untuk detail.'); 24226 return null; 24227 } 24228 } 24229 24230 // Load reviewable orders (for account page) 24231 async function loadReviewableOrders() { 24232 // Only run if on account/profile page 24233 const container = document.getElementById('reviewable-orders-list'); 24234 const loading = document.getElementById('reviewable-orders-loading'); 24235 const empty = document.getElementById('reviewable-orders-empty'); 24236 24237 if (!container) { 24238 return; 24239 } 24240 24241 // Show loading 24242 if (loading) loading.style.display = 'block'; 24243 if (empty) empty.style.display = 'none'; 24244 container.innerHTML = ''; 24245 24246 try { 24247 const response = await fetch('/api/reviews/reviewable.php'); 24248 const result = await response.json(); 24249 24250 if (loading) loading.style.display = 'none'; 24251 24252 if (result.success && result.data.length > 0) {
24253 renderReviewableOrders(result.data); 24254 if (empty) empty.style.display = 'none'; 24255 } else { 24256 if (empty) empty.style.display = 'block'; 24257 } 24258 } catch (error) { 24259 console.error('Load reviewable orders error:', error); 24260 if (loading) loading.style.display = 'none'; 24261 if (empty) { 24262 empty.innerHTML = '<p>Gagal memuat data. Silakan refresh halaman.</p>'; 24263 empty.style.display = 'block'; 24264 } 24265 } 24266 } 24267 24268 // Render reviewable orders 24269 function renderReviewableOrders(orders) { 24270 const container = document.getElementById('reviewable-orders-list'); 24271 if (!container) return; 24272 24273 let html = ''; 24274 24275 orders.forEach(order => { 24276 html += ` 24277 <div class="reviewable-order-item"> 24278 <img src="${escapeHtml(order.product_image || '/assets/img/no-image.png')}" alt="${escapeHtml(order.product_name)}" class="roi-image"> 24279 <div class="roi-info"> 24280 <h4>${escapeHtml(order.product_name)}</h4> 24281 <p class="roi-meta">Order: ${escapeHtml(order.order_number)}</p> 24282 <p class="roi-meta">Dibeli: ${escapeHtml(order.order_date)}</p> 24283 </div> 24284 <button type="button" 24285 class="btn btn-primary btn-write-review" 24286 data-product-id="${order.product_id}" 24287 data-order-id="${order.order_id || ''}" 24288 data-so-ref-id="${escapeHtml(order.so_ref_id || '')}" 24289 data-product-name="${escapeHtml(order.product_name)}" 24290 data-product-image="${escapeHtml(order.product_image || '')}" 24291 data-product-sku="${escapeHtml(order.product_sku || '')}"> 24292 âï¸ Tulis Ulasan 24293 </button> 24294 </div> 24295 `; 24296 }); 24297 24298 container.innerHTML = html; 24299 } 24300 24301 // Escape HTML for XSS protection 24302 function escapeHtml(text) { 24303 const div = document.createElement('div'); 24304 div.textContent = text; 24305 return div.innerHTML; 24306 } 24307 24308 // Show error 24309 function showError(message) { 24310 const errorEl = document.getElementById('review-form-error'); 24311 errorEl.textContent = message; 24312 errorEl.style.display = 'block'; 24313 24314 setTimeout(() => { 24315 errorEl.style.display = 'none'; 24316 }, 5000); 24317 } 24318 24319 // Show success 24320 function showSuccess(message) { 24321 // Create toast notification 24322 const toast = document.createElement('div'); 24323 toast.className = 'toast-notification success'; 24324 toast.textContent = message; 24325 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;'; 24326 24327 document.body.appendChild(toast); 24328 24329 setTimeout(() => { 24330 toast.style.opacity = '0'; 24331 toast.style.transition = 'opacity 0.3s'; 24332 setTimeout(() => toast.remove(), 300); 24333 }, 3000); 24334 } 24335 24336 // Mask user name for privacy preview (like PHP maskReviewerName) 24337 function maskUserName(fullName) { 24338 if (!fullName || fullName.trim() === '') { 24339 return 'Anonymous'; 24340 } 24341 24342 const words = fullName.trim().split(/\s+/); 24343 const maskedWords = words.map(word => { 24344 const len = word.length; 24345 if (len <= 1) return word; 24346 return word.charAt(0) + '*'.repeat(len - 1); 24347 }); 24348 24349 return maskedWords.join(' '); 24350 } 24351 24352 // Update privacy preview text with user's actual masked name 24353 function updatePrivacyPreview() { 24354 const previewEl = document.getElementById('review-anonymous-preview'); 24355 if (!previewEl) return; 24356 24357 const userName = window.userFullName || ''; 24358 if (userName) { 24359 const maskedName = maskUserName(userName); 24360 previewEl.textContent = `Nama Anda akan ditampilkan sebagai "${maskedName}" untuk menjaga privasi`; 24361 } else { 24362 previewEl.textContent = 'Nama Anda akan ditampilkan de
24362ngan disamarkan untuk menjaga privasi'; 24363 } 24364 } 24365 24366 // Initialize when DOM ready 24367 if (document.readyState === 'loading') { 24368 document.addEventListener('DOMContentLoaded', init); 24369 } else { 24370 init(); 24371 } 24372 24373 // Export openReviewModal to global scope for use in other scripts 24374 window.openReviewModal = openReviewModal; 24375 24376})(); 24377 24378</script>
24378 24379 <style> 24380/* ========================================================= 24381 â¡ FIXED FOOTER â RED ELEGANT MODE 24382========================================================= */ 24383 24384.sbo-top-fixed-footer{ 24385 position: fixed; 24386 left: 0; 24387 right: 0; 24388 bottom: 0; 24389 z-index: 999; 24390 display: flex; 24391 align-items: center; 24392 justify-content: space-around; 24393 gap: 8px; 24394 padding: 11px 12px calc(11px + env(safe-area-inset-bottom)); 24395 overflow: hidden; 24396 background: radial-gradient(circle at 50% 0%, #000000, rgb(0 0 0 / 92%) 58%, rgb(0 0 0 / 98%)), linear-gradient(180deg, rgba(25, 0, 0, 0.92), rgb(0 0 0 / 98%)); 24397 backdrop-filter: blur(12px); 24398 border-top: 1px solid rgb(143 1 1); 24399 border-radius: 18px 18px 0 0; 24400 box-shadow: 0 -12px 30px #410000, inset 0 1px 0 rgba(255, 255, 255, .05); 24401 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 24402} 24403 24404/* scanline */ 24405.sbo-top-fixed-footer::before{ 24406 content:""; 24407 position:absolute; 24408 inset:0; 24409 pointer-events:none; 24410 background: 24411 repeating-linear-gradient( 24412 0deg, 24413 rgba(255,255,255,.015), 24414 rgba(255,255,255,.015) 1px, 24415 transparent 1px, 24416 transparent 6px 24417 ); 24418 opacity:.85; 24419 mix-blend-mode:overlay; 24420} 24421 24422/* shine sweep */ 24423.sbo-top-fixed-footer::after{ 24424 content:""; 24425 position:absolute; 24426 top:0; 24427 left:-160%; 24428 width:60%; 24429 height:100%; 24430 background: linear-gradient(120deg, transparent, rgba(255, 90, 90, 0.25), transparent); 24431 transform: skewX(-25deg); 24432 animation: footerFinalShine 3.8s infinite; 24433 mix-blend-mode: screen; 24434 pointer-events:none; 24435} 24436 24437@keyframes footerFinalShine{ 24438 0%{left:-160%;} 24439 68%{left:160%;} 24440 100%{left:160%;} 24441} 24442 24443/* ========================================================= 24444 ITEM 24445========================================================= */ 24446 24447.sbo-top-fixed-footer a{ 24448 flex:1; 24449 max-width:110px; 24450 24451 display:flex; 24452 flex-direction:column; 24453 align-items:center; 24454 justify-content:center; 24455 gap:6px; 24456 24457 padding: 8px 6px; 24458 24459 text-decoration:none; 24460 text-align:center; 24461 24462 color: rgba(171 0 0); 24463 font-size: 12px; 24464 font-weight: 800; 24465 letter-spacing: .25px; 24466 24467 border-radius: 14px; 24468 position:relative; 24469 z-index:1; 24470 24471 transition: all .18s ease; 24472} 24473 24474.sbo-top-fixed-footer a:hover{ 24475 color:#ffffff; 24476 background: rgba(255, 80, 80, 0.08); 24477 filter: drop-shadow(0 0 12px rgba(255, 80, 80, 0.15)); 24478 transform: translateY(-1px); 24479} 24480 24481.sbo-top-fixed-footer a:active{ 24482 transform: translateY(0) scale(.99); 24483} 24484 24485/* ========================================================= 24486 ICON 24487========================================================= */ 24488 24489.sbo-top-fixed-footer img{ 24490 width:26px; 24491 height:26px; 24492 transition: .18s ease; 24493 filter: drop-shadow(0 0 8px rgba(255 242 80 / 30%5)); 24494} 24495 24496.sbo-top-fixed-footer a:hover img{ 24497 transform: scale(1.08); 24498 filter: drop-shadow(0 0 16px rgba(255, 80, 80, 0.6)); 24499} 24500 24501/* ========================================================= 24502 HUD CORNERS 24503========================================================= */ 24504 24505.sbo-top-fixed-footer .hud-corner{ 24506 position:absolute; 24507 width:18px;height:18px; 24508 pointer-events:none; 24509 border: 1px solid rgba(255, 90, 90, 0.5); 24510 filter: drop-shadow(0 0 10px rgba(255, 80, 80, 0.2)); 24511 opacity:.85; 24512} 24513 24514.sbo-top-fixed-footer .hud-tl{top:8px;left:10px;border-right:none;border-bottom:none;border-radius:10px 0 0 0;} 24515.sbo-top-fixed-footer .hud-tr{top:8px;right:10px;border-left:none;border-bottom:none;border-radius:0 10px 0 0;} 24516.sbo-top-fixed-footer .hud-bl{bottom:8px;left:10px;border-right:none;border-top:none;border-radius:0 0 0 10px;} 24517.sbo-top-fixed-footer .hud-br{bottom:8px;right:10px;border-left:none;border-top:none;border-radius:0 0 10px 0;} 24518 24519/* ========================================================= 24520 CENTER CTA 24521========================================================= */ 24522 24523.sbo-top-fixed-footer .tada{ 24524 color: #ffffff; 24525 background: radial-gradient(circle at 50% 0%, #f52b2b, rgba(0, 0, 0, 0) 70%), rgba(255, 60, 60, 0.08); 24526 border: 1px solid rgba(255, 90, 90, 0.35); 24527 box-shadow: 0 0 18px #f52b2b, inset 0 0 16px rgba(255, 60, 60, 0.15); 24528} 24529 24530.sbo-top-fixed-footer .tada img{ 24531 width:30px; 24532 height:30px; 24533 filter: drop-shadow(0 0 16px rgba(255, 80, 80, 0.65)); 24534} 24535 24536/* pulse */ 24537.sbo-top-fixed-footer .tada::before{ 24538 content:""; 24539 position:absolute; 24540 inset:-12px; 24541 border-radius:999px; 24542 background: 24543 radial-gradient(circle, #000000, transparent 70%), 24544 radial-gradient(circle, rgba(255, 120, 120, 0.08), transparent 72%); 24545 filter: blur(12px); 24546 opacity:.55; 24547 animation: corePulse 2.9s ease-in-out infinite; 24548 z-index:-1; 24549} 24550 24551@keyframes corePulse{ 24552 0%,100%{opacity:.35; transform:scale(.99);} 24553 50%{opacity:.75; transform:scale(1.02);} 24554} 24555 24556/* ACTIVE */ 24557.sbo-top-fixed-footer a.is-active{ 24558 color:#ffffff; 24559 background: rgba(255, 80, 80, 0.1); 24560 box-shadow: inset 0 0 0 1px rgba(255, 90, 90, 0.25); 24561} 24562 24563.sbo-top-fixed-footer a.is-active img{ 24564 filter: drop-shadow(0 0 18px rgba(255, 80, 80, 0.8)); 24565} 24566 24567/* RESPONSIVE */ 24568@media (max-width:500px){ 24569 .sbo-top-fixed-footer{ 24570 padding: 10px 10px calc(10px + env(safe-area-inset-bottom)); 24571 border-radius: 16px 16px 0 0; 24572 gap:6px; 24573 } 24574 .sbo-top-fixed-footer a{ 24575 font-size:11px; 24576 padding:7px 5px; 24577 gap:5px; 24578 max-width:104px; 24579 } 24580 .sbo-top-fixed-footer img{width:22px;height:22px;} 24581 .sbo-top-fixed-footer .tada img{width:26px;height:26px;} 24582} 24583</style> 24584<div class="sbo-top-fixed-footer"> 24585 <a href="https://bim.miami/aboutus/" target="_blank" rel="nofollow noopener"> 24586 <img src="https://imgmahasuhu.io/images/2026/05/13/iconbonus.png" alt="BONUS BERLINTOTO"> 24587 Promosi 24588 </a> 24589 <a href="https://bim.miami/aboutus/" target="_blank" rel="nofollow noopener"> 24590 <img src="https://imgmahasuhu.io/images/2026/05/13/iconlogin.png" alt="LOGIN BERLINTOTO"> 24591 Login 24592 </a> 24593 <a href="https://bim.miami/aboutus/" target="_blank" rel="nofollow noopener" class="tada"> 24594 <img src="https://imgmahasuhu.io/images/2026/05/13/icondaftar.webp" alt="DAFTAR BERLINTOTO"> 24595 Daftar 24596 </a> 24597 <a href="https://bim.miami/aboutus/" target="_blank" rel="nofollow noopener"> 24598 <img src="https://imgmahasuhu.io/images/2026/05/13/iconlinkalternatif.png" alt="LINK ALTERNATIF BERLINTOTO"> 24599 Alternatif 24600 </a> 24601 <a href="https://bim.miami/aboutus/" target="_blank" rel="nofollow noopener"> 24602 <img src="https://imgmahasuhu.io/images/2026/05/13/iconlivechat.png" alt="LIVE CHAT BERLINTOTO"> 24603 Live Chat
24604 </a> 24605</div>
24606<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" 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>
vendor: 1 bytes, line 24606
24606
24607<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24607
24607
24608<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24608
24608
24609<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24609
24609
24610<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"6138f29286ee4657aedef37717a3a41a","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24610
24610
24611<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"584e215915fc4a94bc26e2b00099ce04","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24611
24611
24612<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"73e47ca119414820a5354c1e7e4c82fe","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24612
24612
24613<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"d4e1c111d5d6419e9d912cb595e74e73","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24613
24613
24614<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"a97528b5df52422ca8ea8c0d234766d0","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24614
24614
24615<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"8c95032917ae4f3da8bb19ff3e848b2c","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24615
24615
24616<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"cee4242206b04e6d8fc1a0fa16207d5e","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24616
24616
24617<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"1c3a4a5b897742b8b80dc093d821946c","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24617
24617
24618<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"577a11b268174c14a1684d49b7e85c4f","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24618
24618
24619<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"577a11b268174c14a1684d49b7e85c4f","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24619
24619
24620<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"467ad21de1b447a8a311c1acf9547e04","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24620
24620
24621<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"ed7af6afbc08412fa75c251a1332ecfc","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24621
24621
24622<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"cdb9c2a2d72c44fc91f04b655b34b25a","r":1,"spa":2}' crossorigin="anonymous"></script>
24622 24623</body> 24624</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.