1<!DOCTYPE html> 2<html lang="id"> 3<head> 4 <meta charset="UTF-8"> 5 <title>Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi</title> 6 <meta name="description" content="PIONTOGEL merepresentasikan wahana digital dengan artikulasi modern yang mengakomodasi beragam permainan resmi secara terintegrasi. Melalui tata kelola antarmuka yang sistematis, navigasi adaptif, dan aksesibilitas yang optimal, platform ini menghadirkan pengalaman digital yang praktis sekaligus atraktif."> 7 <meta name="robots" content="index,follow"> 8 <link rel="canonical" href="https://calcourtsfitness.com/"> 9 <meta name="google-site-verification" content="6fVXoKpZcx_doXYAZnJ6iZNC63wL6r5Pzw99b7is38E" /> 10 <link rel="amphtml" href="https://calcourtsfitness.nichesite.org/"> 11 <link rel="alternate" href="https://calcourtsfitness.nichesite.org/" hreflang="id"> 12 <link rel="alternate" href="https://calcourtsfitness.nichesite.org/" hreflang="en"> 13 <link rel="alternate" href="https://calcourtsfitness.nichesite.org/" hreflang="x-default"> 14 <link rel="alternate" hreflang="id-ID" href="https://calcourtsfitness.nichesite.org/" /> 15 <link rel="alternate" hreflang="en-US" href="https://calcourtsfitness.nichesite.org/" /> 16 <link rel="alternate" hreflang="en-SG" href="https://calcourtsfitness.nichesite.org/" /> 17 <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
vendor: 67 bytes, lines 17-18
17 18 <script async src="https://www.googletagmanager.com/gtag/js?id=
18G-0LSM1YJSPZ
vendor: 48 bytes, lines 18-19
18" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script> 19
19<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>
19 20 <meta property="og:locale" content="id_ID"> 21 <meta property="og:site_name" content="TOTO TOGEL"> 22 <meta property="og:type" content="product"> 23 <meta property="og:title" content="Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi"> 24 <meta property="og:description" content="PIONTOGEL merepresentasikan wahana digital dengan artikulasi modern yang mengakomodasi beragam permainan resmi secara terintegrasi. Melalui tata kelola antarmuka yang sistematis, navigasi adaptif, dan aksesibilitas yang optimal, platform ini menghadirkan pengalaman digital yang praktis sekaligus atraktif."> 25 <meta property="og:url" content="https://calcourtsfitness.com/"> 26 <meta property="og:image" content="https://a1.asdqwe.vip/banner/piontogel-img1"> 27 <meta property="og:image:alt" content="Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi"> 28<meta property="product:price:amount" content="5000"> 29<meta property="product:price:currency" content="IDR"> 30<meta property="product:availability" content="out of stock"> 31<meta name="twitter:card" content="summary_large_image"> 32<meta name="twitter:title" content="Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi"> 33<meta name="twitter:description" content="PIONTOGEL merepresentasikan wahana digital dengan artikulasi modern yang mengakomodasi beragam permainan resmi secara terintegrasi. Melalui tata kelola antarmuka yang sistematis, navigasi adaptif, dan aksesibilitas yang optimal, platform ini menghadirkan pengalaman digital yang praktis sekaligus atraktif."> 34<meta name="twitter:image" content="https://a1.asdqwe.vip/banner/piontogel-img1"> 35
36<script type="application/ld+json"> 37 { 38 "@context": "http:\/\/schema.org", 39 "@type": "BreadcrumbList", 40 "itemListElement": [ 41 { 42 "@type": "ListItem", 43 "item": { 44 "@id": "https://calcourtsfitness.com/", 45 "name": "Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi" 46 }, 47 "position": 1 48 } 49 ] 50 } 51 </script>
51 52
52<script type="application/ld+json"> 53 { 54 "@context": "http:\/\/schema.org", 55 "@type": "Product", 56 "name": "Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi", 57 "description": "PIONTOGEL merepresentasikan wahana digital dengan artikulasi modern yang mengakomodasi beragam permainan resmi secara terintegrasi. Melalui tata kelola antarmuka yang sistematis, navigasi adaptif, dan aksesibilitas yang optimal, platform ini menghadirkan pengalaman digital yang praktis sekaligus atraktif.", 58 "image": "https://a1.asdqwe.vip/banner/piontogel-img1", 59 "offers": { 60 "@type": "http:\/\/schema.org\/Offer", 61 "price": 9999, 62 "url": "https://calcourtsfitness.com/", 63 "priceCurrency": "IDR", 64 "availability": "http:\/\/schema.org\/InStock" 65 } 66 } 67 </script>
67 68
68<script type="application/ld+json"> 69 { 70 "@context": "http:\/\/schema.org\/", 71 "@type": "WebPage", 72 "speakable": { 73 "@type": "SpeakableSpecification", 74 "cssSelector": [".description"], 75 "xpath": ["\/html\/head\/title"] 76 } 77 } 78 </script>
78 79 <!-- ZS --> 80 <!-- Schema JSON untuk SBO -->
81<script type="application/ld+json"> 82{ 83"@context": "https://schema.org/", 84"@type": "Product", 85"name": "Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi", 86"url": "https://calcourtsfitness.com/", 87"sku": "BANDAR TOGEL", 88"image": [ 89 "https://a1.asdqwe.vip/banner/piontogel-img1" 90], 91"description": "PIONTOGEL merepresentasikan wahana digital dengan artikulasi modern yang mengakomodasi beragam permainan resmi secara terintegrasi. Melalui tata kelola antarmuka yang sistematis, navigasi adaptif, dan aksesibilitas yang optimal, platform ini menghadirkan pengalaman digital yang praktis sekaligus atraktif.", 92"brand": { 93 "@type": "Organization", 94 "name": "TOGEL ONLINE" 95}, 96"offers": { 97 "@type": "Offer", 98 "name": "Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi", 99 "sku": "TOGEL ONLINE", 100 "price": "9999", 101 "priceCurrency": "IDR", 102 "availability": "https://schema.org/InStock" 103} 104} 105</script>
105 106
107<script type="application/ld+json"> 108{ 109"@context": "https://schema.org/", 110"@type": "Product", 111"name": "Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi", 112"image": "https://a1.asdqwe.vip/banner/piontogel-img1", 113"description": "PIONTOGEL merepresentasikan wahana digital dengan artikulasi modern yang mengakomodasi beragam permainan resmi secara terintegrasi. Melalui tata kelola antarmuka yang sistematis, navigasi adaptif, dan aksesibilitas yang optimal, platform ini menghadirkan pengalaman digital yang praktis sekaligus atraktif.", 114"sku": "TOGEL ONLINE", 115"offers": { 116 "@type": "Offer", 117 "priceCurrency": "IDR", 118 "url": "https://calcourtsfitness.com/", 119 "availability": "https://schema.org/InStock", 120 "price": "9999.00" 121} 122} 123</script>
123 124
125<script type="application/ld+json" id="bv-jsonld-bvloader-summary"> 126{ 127"@context": "https://schema.org/", 128"@id": "https://calcourtsfitness.com/", 129"aggregateRating": { 130 "@type": "AggregateRating", 131 "itemReviewed": { 132 "@type": "IndividualProduct", 133 "name": "BANDAR TOGEL" 134 }, 135 "ratingValue": "5.0", 136 "reviewCount": 954638, 137 "bestRating": 5 138} 139} 140</script>
140 141
142<script type="application/ld+json"> 143{ 144"@context": "https://schema.org", 145"@type": "BreadcrumbList", 146"itemListElement": [ 147 { 148 "@type": "ListItem", 149 "position": 1, 150 "item": { 151 "@id": "https://calcourtsfitness.com/", 152 "name": "BANDAR TOGEL" 153 } 154 }, 155 { 156 "@type": "ListItem", 157 "position": 2, 158 "item": { 159 "@id": "https://calcourtsfitness.com/", 160 "name": "Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi" 161 } 162 }, 163 { 164 "@type": "ListItem", 165 "position": 3, 166 "item": { 167 "@id": "https://calcourtsfitness.com/", 168 "name": "TOGEL ONLINE" 169 } 170 }, 171 { 172 "@type": "ListItem", 173 "position": 4, 174 "item": { 175 "@id": "https://calcourtsfitness.com/", 176 "name": "Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi" 177 } 178 } 179] 180} 181</script>
181 182
183<script type="application/ld+json" id="bv-jsonld-reviews-data"> 184{ 185 "@context": "https://schema.org", 186 "@type": "Organization", 187 "@id": "https://calcourtsfitness.com/", 188 "name": "BANDAR TOGEL ONLINE", 189 "alternateName": "BANDAR TOGEL ONLINE", 190 "url": "https://calcourtsfitness.com/", 191 "review": [ 192 { 193 "@type": "Review", 194 "dateCreated": "2026-03-31T12:28:35+00:00", 195 "datePublished": "2026-03-31T12:28:35+00:00", 196 "headline": "TOGEL ONLINE", 197 "reviewBody": "Yang bikin saya tertarik adalah pilihan permainannya cukup beragam. Jadi bisa melihat-lihat dulu sebelum menentukan pilihan.", 198 "reviewRating": { 199 "@type": "Rating", 200 "ratingValue": 5, 201 "bestRating": 5, 202 "worstRating": 1 203 }, 204 "author": { 205 "@type": "Person", 206 "name": "Rian Pratama â Surabaya" 207 } 208 }, 209 { 210 "@type": "Review", 211 "dateCreated": "2026-04--02T18:28:21+00:00", 212 "datePublished": "2026-04-03T18:28:21+00:00", 213 "headline": "TOGEL ONLINE", 214 "reviewBody": "Pasarannya lengkap dan riwayat keluaran tersusun rapi. Saya jadi lebih mudah membandingkan result sekaligus melihat pola angka yang sering muncul.", 215 "reviewRating": { 216 "@type": "Rating", 217 "ratingValue": 5, 218 "bestRating": 5, 219 "worstRating": 1 220 }, 221 "author": { 222 "@type": "Person", 223 "name": "Vinka Aurelia â Semarang" 224 } 225 }, 226 { 227 "@type": "Review", 228 "dateCreated": "2026-04-05T19:28:18+00:00", 229 "datePublished": "2026-04-05T19:28:18+00:00", 230 "headline": "TOGEL ONLINE", 231 "reviewBody": "Menurut saya konsepnya cukup beda. Informasi yang ditampilkan nggak terasa terlalu penuh dan masih nyaman buat dijelajahi.", 232 "reviewRating": { 233 "@type": "Rating", 234 "ratingValue": 5, 235 "bestRating": 5, 236 "worstRating": 1 237 }, 238 "author": { 239 "@type": "Person", 240 "name": "Violet Evergarde â Makassar" 241 } 242 } 243 ] 244} 245</script>
vendor: 2 bytes, line 245
245
246<script type="application/ld+json"> 247{ 248 "@context": "https://schema.org", 249 "@type": "FAQPage", 250 "mainEntity": [ 251 { 252 "@type": "Question", 253 "name": "Apa yang menjadi ciri khas PIONTOGEL?", 254 "acceptedAnswer": { 255 "@type": "Answer", 256 "text": "PIONTOGEL mengusung konsep ruang digital yang memadukan keberagaman permainan, penyajian informasi, dan pengalaman penggunaan dalam satu platform." 257 } 258}, 259{ 260 "@type": "Question", 261 "name": "Mengapa keberagaman permainan menjadi bagian penting?", 262 "acceptedAnswer": { 263 "@type": "Answer", 264 "text": "Keberagaman memberikan keleluasaan bagi pengguna untuk mengenali berbagai pilihan yang tersedia sesuai dengan ketertarikannya masing-masing." 265 } 266}, 267{ 268 "@type": "Question", 269 "name": "Apa yang dimaksud dengan perspektif berbeda pada PIONTOGEL?", 270 "acceptedAnswer": { 271 "@type": "Answer", 272 "text": "Perspektif tersebut merujuk pada cara platform menyusun informasi dan menghadirkan pilihan hiburan secara lebih terarah, bukan sekadar menampilkan banyak elemen sekaligus." 273 } 274}, 275{ 276 "@type": "Question", 277 "name": "Bisa PIONTOGEL diakses menggunakan ponsel?", 278 "acceptedAnswer": { 279 "@type": "Answer", 280 "text": "Pengguna dapat melihat kategori dan informasi yang disediakan pada platform, kemudian memilih bagian yang ingin dieksplorasi berdasarkan keterangannya." 281 } 282}, 283{ 284 "@type": "Question", 285 "name": "Apakah PIONTOGEL cocok untuk pengguna baru?", 286 "acceptedAnswer": { 287 "@type": "Answer", 288 "text": "Platform dengan penyajian informasi yang terstruktur dapat membantu pengguna baru mengenali berbagai bagian secara bertahap tanpa harus memahami seluruh fitur sekaligus." 289 } 290} 291 ] 292} 293</script>
293 294 <!-- User Session Data --> 295 <meta name="csrf-token" content="9ce6e4a17f7cc189893df9e47789b13e98829450d516a0cae5ca7b5f43e5dadc"> 296 <meta name="user-logged-in" content="false"> 297 <meta name="user-id" content="0"> 298 299 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> 300 301 <link rel="shortcut icon" href="https://a1.asdqwe.vip/assets/icon-piontogel.png" type="image/x-icon"> 302 <style data-inlined="style.css"> 303@font-face { 304 font-family: 'Segoe UI'; 305 src: url('/assets/fonts/segoeui.woff2') format('woff2'), 306 url('/assets/fonts/segoeui.woff') format('woff'); 307 font-weight: 400; 308 font-style: normal; 309} 310 311:root { 312 --brand-50:#FFF5E6; 313 --brand-100:#FFE7CC; 314 --brand-500:#F28C1A; 315 --brand-600:#D97809; 316 --gray-900:#1F1F23; 317 --gray-700:#3D3F45; 318 --gray-500:#6F737B; 319 --gray-300:#C5C9CF; 320 --gray-100:#F3F5F7; 321 --border:#E4E7EB; 322 --radius:8px; 323 --transition: .18s ease; 324 /* font-family: "Inter", system-ui, sans-serif; */ 325 font-family: 'Segoe UI', sans-serif; 326} 327 328* { box-sizing: border-box; } 329body { 330 margin:0; 331 padding-bottom: 0; /* Reset for desktop */ 332 color:var(--gray-900); 333 background:#fff; 334} 335img { max-width:100%; display:block; } 336 337.container { 338 width:100%; 339 max-width:1280px; 340 margin:0 auto; 341 padding:10px 0; 342} 343 344.topbar { 345 background:#111; 346 color:#fff; 347 font-size:13px; 348} 349.topbar-inner { 350 display:flex; 351 justify-content:space-between; 352 align-items:center; 353 min-height:36px; 354} 355.topbar a { color:#fff; text-decoration:none; margin-left:12px; } 356 357/* ========== Desktop base (Glassmorphism Modern with Warm Gradient) ========== */ 358.navbar { 359 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 360 backdrop-filter:blur(12px); 361 -webkit-backdrop-filter:blur(12px); 362 border-bottom:1px solid rgba(228,231,235,0.6); 363 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 364 position:sticky; 365 top:0; 366 z-index:500; 367 transition:all .3s ease; 368} 369 370/* Accent line gradient orange */ 371.navbar::after { 372 content:''; 373 position:absolute; 374 bottom:0; 375 left:0; 376 right:0; 377 height:2px; 378 background:linear-gradient(90deg,transparent 0%, var(--brand-500) 20%, var(--brand-600) 50%, var(--brand-500) 80%, transparent 100%); 379 opacity:0.5; 380} 381 382.nav-inner { 383 display:flex; 384 align-items:center; 385 gap:24px; 386 min-height:70px; 387} 388.nav-inner .search .m-search-ico { display: none; } 389 390/* Secara default (desktop) ikon login disembunyikan; teks muncul */ 391.login-btn .login-ico { display:none; } 392.navbar-logo { 393 display: none; 394 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 395 backdrop-filter:blur(12px); 396 -webkit-backdrop-filter:blur(12px); 397 border-bottom:1px solid rgba(228,231,235,0.6); 398 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 399 position:sticky; 400 top:0; 401 z-index:500; 402} 403 404/* Hide bottom navigation on desktop - only show on mobile */ 405.bottom-nav { 406 display: none; 407} 408 409/* ============================================================== 410 MOBILE (<=640px) 411 ============================================================== */ 412@media (max-width:640px){ 413 .container { padding:10px 16px; } 414 415 .topbar { display:none !important; } 416 417 /* Add padding bottom for sticky bottom nav */ 418 body { 419 padding-bottom: 68px; /* Height of bottom nav + safe area */ 420 } 421 422 /* Mobile Top Navbar - Clean & Minimal */ 423 .navbar { 424 background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); 425 backdrop-filter:blur(16px); 426 -webkit-backdrop-filter:blur(16px); 427 box-shadow:0 2px 12px -3px rgba(0,0,0,0.08); 428 border: 0; 429 } 430 431 /* Simple grid layout - Logo, Search, Category */ 432 .nav-inner { 433 display:grid; 434 grid-template-columns:1fr; 435 gap:10px; 436 padding:12px 16px 0 12px; 437 min-height:auto; 438 } 439 440 /* Logo centered at top */ 441 .nav-inner .logo { 442 justify-self:center; 443 margin:0; 444 display:flex; 445 } 446 .nav-inner .logo img { height:36px; } 447 448 /* Search bar - full width */ 449 .nav-inner .search { 450 position:relative; 451 display:flex;
452 align-items:center; 453 gap:0; 454 width:100%; 455 height:44px; 456 background:#fff; 457 border:1.5px solid #e5e7eb; 458 border-radius:12px; 459 padding:0 12px; 460 box-shadow:0 1px 3px rgba(0,0,0,0.04); 461 transition:all .25s; 462 } 463 .nav-inner .search:focus-within { 464 border-color:#fb923c; 465 box-shadow:0 0 0 3px rgba(251, 146, 60, 0.1); 466 } 467 468 .nav-inner .search-icon-btn { 469 color:#9ca3af; 470 padding:4px; 471 margin-right:4px; 472 } 473 474 .nav-inner .search-icon-btn svg { 475 width:18px; 476 height:18px; 477 } 478 479 .nav-inner .search input { 480 flex:1; 481 min-width:0; 482 border:none; 483 outline:none; 484 background:transparent; 485 font-size:14px; 486 padding:0 8px; 487 height:100%; 488 } 489 490 .nav-inner .ai-bot-toggle { 491 padding:6px; 492 margin-left:0; 493 } 494 495 .nav-inner .ai-bot-toggle .bot-icon { 496 width:20px; 497 height:20px; 498 } 499 500 .nav-inner .search-suggest { 501 left:0; 502 right:0; 503 width:calc(100vw - 32px); 504 margin-top:6px; 505 border-radius:12px; 506 max-height:70vh; 507 } 508 509 /* Hide desktop nav actions but keep structure for userMenu */ 510 .nav-inner .nav-actions { 511 display:flex !important; /* Keep flex to allow userMenu positioning */ 512 visibility:hidden; /* Hide visually but keep in DOM */ 513 pointer-events:none; /* Disable interactions with hidden elements */ 514 position:absolute; /* Remove from flow */ 515 right:0; 516 top:0; 517 } 518 519 /* Re-enable userMenu for logged-in users */ 520 .nav-inner .nav-actions #userMenu { 521 visibility:visible !important; 522 pointer-events:auto !important; 523 } 524 525 .nav-inner .logo { display: none; } 526 .navbar-logo { 527 display: block; 528 /* background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); */ 529 backdrop-filter:blur(16px); 530 -webkit-backdrop-filter:blur(16px); 531 position: static; /* Remove sticky on mobile */ 532 box-shadow: none; 533 border-bottom: 0; 534 padding-top: .6rem; 535 } 536 .nav-inner .logo-mobile { display: block; } 537 538 /* Category menu wrapper will handle visibility */ 539 540 /* ============================================================== 541 BOTTOM NAVIGATION - Styles moved to bottom-nav.css 542 ============================================================== */ 543 /* All bottom-nav styles now in /assets/css/bottom-nav.css */ 544 545 /* Hide desktop elements that now moved to bottom nav */ 546 #wishlistBtn, 547 #notifBtn, 548 #loginBtn { display:none !important; } 549 550 /* Hide desktop user menu on mobile - use bottom sheet instead */ 551 .navbar #userMenu { 552 display:none !important; 553 } 554 555 .nav-actions .cart-indicator, 556 .nav-actions .compare-indicator { 557 display:none !important; 558 } 559 560 /* Hide category button from bottom nav */ 561 #bottomCategoryBtn { 562 display: none !important; 563 } 564 565 /* User menu button styling for mobile - scoped to navbar only */ 566 .navbar #userMenu .login-btn.logged-in { 567 width:40px; 568 height:40px; 569 padding:0; 570 border-radius:50%; 571 display:flex;
572 align-items:center; 573 justify-content:center; 574 background:rgba(255,255,255,0.95); 575 backdrop-filter:blur(12px); 576 -webkit-backdrop-filter:blur(12px); 577 border:1px solid rgba(228,231,235,0.6); 578 box-shadow:0 2px 12px -3px rgba(0,0,0,0.12); 579 } 580 581 .navbar #userMenu .login-btn.logged-in .login-text { 582 display:none; 583 } 584 585 .navbar #userMenu .login-btn.logged-in .login-ico { 586 display:inline; 587 font-size:20px; 588 line-height:1; 589 } 590 591 /* User dropdown positioning for mobile */ 592 .navbar #userMenu .user-dropdown { 593 position:fixed !important; 594 top:60px !important; 595 right:16px !important; 596 left:auto !important; 597 min-width:200px; 598 background:rgba(255,255,255,0.98); 599 backdrop-filter:blur(20px); 600 -webkit-backdrop-filter:blur(20px); 601 border:1px solid rgba(228,231,235,0.6); 602 box-shadow:0 8px 24px -8px rgba(0,0,0,0.2); 603 z-index:1002 !important; 604 border-radius:12px; 605 padding:8px 0; 606 } 607 608 /* Force visibility when not hidden attribute */ 609 .navbar #userMenu .user-dropdown:not([hidden]) { 610 display:block !important; 611 visibility:visible !important; 612 opacity:1 !important; 613 } 614 615 /* Container untuk kategori dan tombol kategori sejajar */ 616 .navbar .container:last-child { 617 display:flex !important; 618 align-items:center !important; 619 padding:4px 16px 6px !important; 620 gap:5px; 621 overflow:visible; 622 } 623 624 /* Wrapper untuk mega-cats dengan fade gradient (mobile only) */ 625 .mega-cats-wrapper { 626 position:relative; 627 flex:1; 628 overflow:hidden; 629 min-height:34px; /* Ensure content is visible */ 630 display:flex; /* Ensure wrapper is flex container */ 631 } 632 633 /* Fade gradient indicator di ujung kanan (mobile only) */ 634 .mega-cats-fade { 635 position:absolute; 636 top:0; 637 right:0; 638 bottom:0; 639 width:80px; /* Increased from 60px for better visibility */ 640 background:linear-gradient(to left, 641 rgba(255,245,230,1) 0%, 642 rgba(255,245,230,0.98) 15%, 643 rgba(255,245,230,0.85) 35%, 644 transparent 100%); 645 pointer-events:none; 646 z-index:5; /* Increased z-index */ 647 transition:opacity 0.3s ease; 648 } 649 650 /* Kategori menu di mobile */ 651 .mega-cats.container { 652 display:flex !important; 653 gap:12px; 654 overflow-x:auto; 655 overflow-y:visible; 656 -webkit-overflow-scrolling:touch; 657 scrollbar-width:none; 658 flex:1; 659 padding:0 !important; 660 margin:0 !important; 661 position:relative; /* For z-index stacking */ 662 z-index:1; /* Below fade gradient */ 663 } 664 .mega-cats.container::-webkit-scrollbar { display:none; } 665 666 /* Styling untuk menu items di mobile */ 667 .mega-cats a { 668 font-size:13px; 669 padding:6px 10px; 670 white-space:nowrap; 671 flex-shrink:0; 672 position:relative; /* Ensure proper stacking */ 673 } 674 .mega-cats a.active { 675 background:var(--brand-600); 676 color:#fff; 677 font-weight:600; 678 border-radius:6px; 679 } 680 681 /* Tombol kategori di mobile */ 682 .mm-wp { 683 flex-shrink:0; 684 } 685 .mm2-trigger { 686 display:flex;
687 align-items:center; 688 gap:6px; 689 padding:6px 12px; 690 background:var(--gray-100); 691 color:var(--gray-700); 692 text-decoration:none; 693 border-radius:6px; 694 font-size:13px; 695 white-space:nowrap; 696 } 697 698 .mm2-trigger .label-cat {display: none; } 699 /* Tombol more kategori */ 700 .mega-more-btn { 701 border:1px solid var(--border); 702 background:#fff; 703 border-radius:10px; 704 padding:6px 14px; 705 font-size:18px; 706 cursor:pointer; 707 } 708 .mega-cats-more.active > .mega-more-btn { 709 background:var(--brand-600); 710 color:#fff; 711 border-color:var(--brand-600); 712 } 713 .mega-more-dropdown { 714 top:calc(100% + 10px); 715 right:0; 716 min-width:85vw; 717 max-width:90vw; 718 border-radius:16px; 719 } 720 .mega-more-grid { 721 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 722 } 723 724 /* ============================================================== 725 MOBILE USER BOTTOM SHEET 726 ============================================================== */ 727 .mobile-user-sheet { 728 position:fixed; 729 top:0; 730 left:0; 731 right:0; 732 bottom:0; 733 z-index:10000; 734 pointer-events:none; 735 } 736 737 .mobile-user-sheet.active { 738 pointer-events:auto; 739 } 740 741 .mobile-user-sheet-backdrop { 742 position:absolute; 743 top:0; 744 left:0; 745 right:0; 746 bottom:0; 747 background:rgba(0,0,0,0.5); 748 opacity:0; 749 transition:opacity .3s ease; 750 } 751 752 .mobile-user-sheet.active .mobile-user-sheet-backdrop { 753 opacity:1; 754 } 755 756 .mobile-user-sheet-content { 757 position:absolute; 758 bottom:0; 759 left:0; 760 right:0; 761 background:#fff; 762 border-radius:20px 20px 0 0; 763 box-shadow:0 -4px 24px rgba(0,0,0,0.15); 764 transform:translateY(100%); 765 transition:transform .3s cubic-bezier(0.4, 0.0, 0.2, 1); 766 padding-bottom:calc(68px + env(safe-area-inset-bottom)); 767 } 768 769 .mobile-user-sheet.active .mobile-user-sheet-content { 770 transform:translateY(0); 771 } 772 773 .mobile-user-sheet-header { 774 padding:16px 20px 12px; 775 border-bottom:1px solid #e5e7eb; 776 } 777 778 .mobile-user-sheet-handle { 779 width:40px; 780 height:4px; 781 background:#d1d5db; 782 border-radius:2px; 783 margin:0 auto 16px; 784 } 785 786 .mobile-user-info { 787 display:flex;
788 align-items:center; 789 gap:12px; 790 } 791 792 .mobile-user-avatar { 793 width:48px; 794 height:48px; 795 border-radius:50%; 796 background:linear-gradient(135deg, #FFE7CC, #FFD9A8); 797 display:flex; 798 align-items:center; 799 justify-content:center; 800 font-size:24px; 801 flex-shrink:0; 802 } 803 804 .mobile-user-details { 805 flex:1; 806 min-width:0; 807 } 808 809 .mobile-user-name { 810 font-size:16px; 811 font-weight:600; 812 color:#111827; 813 margin-bottom:2px; 814 white-space:nowrap; 815 overflow:hidden; 816 text-overflow:ellipsis; 817 } 818 819 .mobile-user-email { 820 font-size:13px; 821 color:#6b7280; 822 white-space:nowrap; 823 overflow:hidden; 824 text-overflow:ellipsis; 825 } 826 827 .mobile-user-sheet-body { 828 padding:8px 0; 829 } 830 831 .mobile-user-sheet-item { 832 display:flex; 833 align-items:center; 834 gap:16px; 835 padding:14px 20px; 836 text-decoration:none; 837 color:#374151; 838 font-size:15px; 839 transition:background .2s; 840 border:none; 841 background:none; 842 width:100%; 843 text-align:left; 844 cursor:pointer; 845 } 846 847 .mobile-user-sheet-item:active { 848 background:#f3f4f6; 849 } 850 851 .mobile-user-sheet-icon { 852 font-size:20px; 853 width:24px; 854 text-align:center; 855 flex-shrink:0; 856 } 857 858 .mobile-user-sheet-label { 859 flex:1; 860 font-weight:500; 861 } 862 863 .mobile-user-sheet-divider { 864 height:1px; 865 background:#e5e7eb; 866 margin:8px 0; 867 } 868} 869 870/* END NAVBAR */ 871 872.logo img { display:block; } 873.search { 874 flex:1; 875 position:relative; 876 display:flex; 877 align-items:center; 878 gap:0; 879 background:#fff; 880 border:1.5px solid #e5e7eb; 881 border-radius:12px; 882 padding:0 16px; 883 height:38px; 884 transition:all 0.3s ease; 885} 886.search:focus-within { 887 border-color:#fb923c; 888 box-shadow:0 0 0 3px rgba(251, 146, 60, 0.1); 889} 890.search input { 891 flex:1; 892 border:none; 893 outline:none; 894 background:transparent; 895 font-size:15px; 896 padding:0 12px; 897 height:100%; 898 color:#1f2937; 899} 900.search input::placeholder { 901 color:#9ca3af; 902} 903.search-icon-btn { 904 background:transparent; 905 border:none; 906 padding:0; 907 display:flex; 908 align-items:center; 909 justify-content:center; 910 cursor:pointer; 911 color:#6b7280; 912 transition:color 0.2s ease; 913 flex-shrink:0; 914} 915.search-icon-btn:hover { 916 color:#fb923c; 917} 918.search-suggest { 919 position:absolute; 920 left:0; right:0; top:100%; 921 background:#fff; 922 border:1px solid var(--border); 923 border-radius:var(--radius); 924 padding:8px 0; 925 box-shadow:0 8px 20px rgba(0,0,0,.08); 926 margin-top:4px; 927 font-size:14px; 928 max-height:260px; 929 overflow:auto; 930} 931.search-suggest div { 932 padding:6px 12px; 933 cursor:pointer; 934} 935.search-suggest div:hover { background:var(--gray-100); } 936 937.nav-actions { display:flex; gap:10px; align-items:center; } 938.icon-btn, .login-btn { 939 background:linear-gradient(135deg, rgba(255,255,255,0.95), rgba(249,250,251,0.9)); 940 border:1px solid rgba(228,231,235,0.6); 941 padding:8px 12px; 942 border-radius:var(--radius); 943 cursor:pointer; 944 font-size:14px; 945 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 946 height: 38px; 947 position:relative; 948 box-shadow: 949 0 1px 3px rgba(0,0,0,0.06), 950 0 0 0 1px rgba(255,255,255,0.5) inset; 951 backdrop-filter:blur(8px); 952 -webkit-backdrop-filter:blur(8px); 953} 954.icon-btn:hover, .login-btn:hover { 955 background:linear-gradient(135deg, rgba(249,250,251,1), rgba(243,244,246,0.95)); 956 border-color:rgba(209,213,219,0.8); 957 box-shadow: 958 0 2px 8px rgba(0,0,0,0.08), 959 0 0 0 1px rgba(255,255,255,0.6) inset; 960 transform:translateY(-1px); 961} 962.cart-indicator, .compare-indicator { position:relative; } 963.badge { 964 background:linear-gradient(135deg, #EF4444, #DC2626); 965 color:#fff; 966 font-size:10.5px; 967 font-weight:600; 968 padding:2px 6px; 969 border-radius:999px; 970 position:absolute; 971 top:-6px; 972 right:-6px; 973 min-width:18px; 974 height:18px; 975 display:flex; 976 align-items:center; 977 justify-content:center; 978 box-shadow: 979 0 2px 8px rgba(239,68,68,0.4), 980 0 0 0 2px #fff, 981 0 0 12px -2px rgba(239,68,68,0.6); 982 transition:all 0.3s ease; 983} 984.icon-btn:hover .badge { 985 transform:scale(1.08); 986 box-shadow: 987 0 4px 12px rgba(239,68,68,0.5), 988 0 0 0 2px #fff, 989 0 0 16px -2px rgba(239,68,68,0.7); 990} 991.badge.is-empty { display:none; } 992 993/* User Menu Dropdown */ 994.user-menu { 995 position:relative; 996} 997.user-dropdown { 998 position:absolute; 999 top:calc(100% + 8px); 1000 right:0; 1001 background:#fff; 1002 border:1px solid var(--border); 1003 border-radius:12px; 1004 box-shadow:0 10px 40px rgba(0,0,0,0.12);
1005 min-width:200px; 1006 padding:8px 0; 1007 z-index:9999; 1008} 1009 1010/* Ensure dropdown is visible when not hidden */ 1011.user-dropdown:not([hidden]) { 1012 display:block !important; 1013 visibility:visible !important; 1014 opacity:1 !important; 1015} 1016 1017/* Mobile: Force user dropdown to be visible above everything */ 1018@media (max-width:640px) { 1019 .user-dropdown:not([hidden]) { 1020 position:fixed !important; 1021 z-index:9999 !important; 1022 top:60px !important; 1023 right:16px !important; 1024 } 1025} 1026 1027.dropdown-item { 1028 display:flex; 1029 align-items:center; 1030 gap:12px; 1031 padding:10px 16px; 1032 text-decoration:none; 1033 color:var(--gray-700); 1034 font-size:14px; 1035 transition:var(--transition); 1036 border:none; 1037 background:none; 1038 width:100%; 1039 text-align:left; 1040 cursor:pointer; 1041} 1042.dropdown-item:hover { 1043 background:var(--gray-100); 1044 color:var(--gray-900); 1045} 1046.dropdown-icon { 1047 font-size:16px; 1048 opacity:0.7; 1049} 1050.dropdown-separator { 1051 height:1px; 1052 background:var(--border); 1053 margin:8px 0; 1054} 1055.login-btn.logged-in { 1056 background:linear-gradient(135deg, rgba(255,245,230,0.8), rgba(255,231,204,0.6)); 1057 border:1px solid rgba(242,140,26,0.2); 1058 color:var(--brand-700); 1059 max-width:200px; /* Prevent button from getting too wide */ 1060 display:inline-flex; 1061 align-items:center; 1062 gap:10px; 1063 padding:4px 12px 4px 4px; 1064 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1065 position:relative; 1066 overflow:hidden; 1067 box-shadow: 1068 0 1px 3px rgba(242,140,26,0.08), 1069 0 0 0 1px rgba(255,255,255,0.5) inset, 1070 0 0 12px -6px rgba(242,140,26,0.1); 1071 backdrop-filter:blur(8px); 1072 -webkit-backdrop-filter:blur(8px); 1073} 1074/* Subtle background glow effect */ 1075.login-btn.logged-in::before { 1076 content:''; 1077 position:absolute; 1078 top:50%; 1079 left:0; 1080 width:32px; 1081 height:32px; 1082 background:radial-gradient(circle, rgba(242,140,26,0.15), transparent 70%); 1083 transform:translateY(-50%); 1084 opacity:0; 1085 transition:opacity 0.3s ease; 1086} 1087.login-btn.logged-in:hover::before { 1088 opacity:1; 1089} 1090.login-btn.logged-in .user-avatar { 1091 width:32px; 1092 height:32px; 1093 border-radius:50%; 1094 background:linear-gradient(135deg, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 1095 color:#D97809; 1096 font-size:11.5px; 1097 font-weight:700; 1098 display:flex; 1099 align-items:center; 1100 justify-content:center; 1101 flex-shrink:0; 1102 letter-spacing:0.3px; 1103 position:relative; 1104 box-shadow: 1105 0 2px 8px rgba(242,140,26,0.25), 1106 0 1px 3px rgba(0,0,0,0.12), 1107 inset 0 1px 0 rgba(255,255,255,0.5); 1108 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1109} 1110/* Ring gradient border */ 1111.login-btn.logged-in .user-avatar::before { 1112 content:''; 1113 position:absolute; 1114 inset:-2px; 1115 border-radius:50%; 1116 padding:2px; 1117 background:linear-gradient(135deg, #F28C1A, #FFD9A8, #F28C1A); 1118 -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1119 -webkit-mask-composite:xor; 1120 mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1121 mask-composite:exclude; 1122 opacity:0; 1123 transition:opacity 0.3s ease; 1124} 1125.login-btn.logged-in:hover .user-avatar::before { 1126 opacity:1; 1127} 1128.login-btn.logged-in:hover .user-avatar { 1129 transform:scale(1.08) rotate(-3deg); 1130 box-shadow: 1131 0 4px 12px rgba(242,140,26,0.35), 1132 0 2px 6px rgba(0,0,0,0.15), 1133 inset 0 1px 0 rgba(255,255,255,0.6); 1134} 1135.login-btn.logged-in .login-text { 1136 overflow:hidden; 1137 text-overflow:ellipsis; 1138 white-space:nowrap; 1139 flex:1; 1140 min-width:0; 1141 font-weight:500; 1142 font-size:14px; 1143 transition:color 0.2s ease; 1144} 1145.login-btn.logged-in:hover .login-text { 1146 color:var(--brand-600); 1147} 1148.login-btn.logged-in:hover { 1149 background:linear-gradient(135deg, rgba(255,245,230,0.95), rgba(255,231,204,0.85)); 1150 border-color:rgba(242,140,26,0.3); 1151 box-shadow: 1152 0 4px 16px -4px rgba(242,140,26,0.25), 1153 0 0 0 1px rgba(242,140,26,0.1) inset, 1154 0 1px 2px rgba(0,0,0,0.05); 1155 transform:translateY(-1px); 1156} 1157 1158/* Desktop: hide wrapper elements, show menu normally */ 1159@media (min-width: 761px) { 1160 .mega-cats-wrapper { 1161 display:contents; /* Remove wrapper from layout */ 1162 } 1163 .mega-cats-fade { 1164 display:none !important; /* Hide fade on desktop */ 1165 } 1166} 1167 1168.mega-cats { 1169 display:flex; 1170 gap:5px; 1171 /* padding:6px 16px 10px; */ 1172 scrollbar-width:none; 1173 position: relative; 1174 z-index: 510; 1175 overflow-x:auto; 1176 -webkit-overflow-scrolling:touch;
1177 flex:1; 1178} 1179.mega-cats::-webkit-scrollbar { 1180 display:none; 1181} 1182.mega-cats a { 1183 text-decoration:none; 1184 font-size:14px; 1185 color:var(--gray-700); 1186 padding:6px 10px; 1187 border-radius:6px; 1188 white-space:nowrap; 1189 transition:var(--transition); 1190} 1191.mega-cats a:hover { background:var(--brand-100); color:var(--brand-600); } 1192/*.mega-cats::after { 1193 content:''; 1194 position:absolute; 1195 right:0; 1196 top:0; 1197 width:40px; 1198 height:100%; 1199 background:linear-gradient(to right,rgba(255,255,255,0),#fff); 1200 pointer-events:none; 1201}*/ 1202 1203/* Tambahan untuk tombol More & dropdown. 1204 Style dasar .mega-cats dan .mega-cats a tetap pakai definisi Anda. */ 1205 1206.mega-cats-more { 1207 position:relative; 1208 display:flex; 1209 align-items:center; 1210} 1211 1212.mega-cats-more.active > .mega-more-btn { 1213 background:var(--brand-600); 1214 color:#fff; 1215} 1216 1217.mega-more-btn { 1218 cursor:pointer; 1219 border:none; 1220 background:var(--gray-100,#f5f5f5); 1221 color:var(--gray-700,#333); 1222 padding:6px 12px; 1223 font-size:16px; 1224 line-height:1; 1225 border-radius:6px; 1226 display:flex; 1227 align-items:center; 1228 justify-content:center; 1229 transition:var(--transition); 1230} 1231.mega-more-btn:hover { 1232 background:var(--brand-100); 1233 color:var(--brand-600); 1234} 1235 1236.mega-more-dropdown { 1237 position:absolute; 1238 top:100%; 1239 right:0; 1240 margin-top:8px; 1241 background:#fff; 1242 border:1px solid var(--gray-200,#e3e6e9); 1243 border-radius:10px; 1244 padding:12px 14px 14px; 1245 min-width:400px; 1246 max-width:520px; 1247 max-height:420px; 1248 box-shadow:0 12px 32px -8px rgba(25,40,65,.25); 1249 z-index: 3000; 1250} 1251 1252.mega-more-grid { 1253 display:grid; 1254 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 1255 gap:8px 10px; 1256 overflow-x: hidden; 1257 max-height:380px; 1258 overscroll-behavior:contain; 1259 scrollbar-width:thin; 1260} 1261.mega-more-grid::-webkit-scrollbar { 1262 width:6px; 1263} 1264.mega-more-grid::-webkit-scrollbar-thumb { 1265 background:var(--gray-300,#c7ccd1); 1266 border-radius:3px; 1267} 1268 1269/* Link di dropdown mewarisi style .mega-cats a (warna, padding, radius). 1270 Kita tambahkan state active agar konsisten. */ 1271.mega-more-grid a.active { 1272 background:var(--brand-600); 1273 color:#fff; 1274 font-weight:600; 1275} 1276.hero { 1277 background:linear-gradient(135deg,var(--brand-50), #fff); 1278} 1279.hero-inner { 1280 display:grid; 1281 grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); 1282 gap:48px; 1283 align-items:center; 1284} 1285.hero-text h1 { 1286 font-size:48px; 1287 line-height:1.1; 1288 margin:0; 1289 font-weight:600; 1290 font-family:"Poppins",sans-serif; 1291} 1292.hero-text .text-brand { color:var(--brand-500); } 1293.lead { font-size:18px; color:var(--gray-700); margin-top:16px; } 1294.hero-cta { margin-top:28px; display:flex; gap:16px; } 1295.hero-bullets { margin-top:28px; display:flex; gap:28px; font-size:14px; color:var(--gray-600); } 1296.hero-box { 1297 background:#fff; 1298 padding:10px; 1299 border-radius:16px; 1300 box-shadow:0 4px 18px rgba(0,0,0,.08); 1301 /* min-height:320px; */ 1302 display:flex; 1303 align-items:center; 1304 justify-content:center; 1305} 1306.hero-box img {border-radius: 10px; } 1307 1308@media (max-width:680px) { 1309 .hero-inner { gap: 25px; } 1310} 1311 1312.usp-section { 1313 position:relative; 1314 background: 1315 linear-gradient(180deg,#FFFDFB 0%, #FFFFFF 60%) ; 1316 border-top:1px solid var(--border); 1317 border-bottom:1px solid var(--border); 1318 padding:10px 0 10px; 1319} 1320 1321.usp-list { 1322 list-style:none; 1323 margin:0; 1324 padding:0; 1325 display:grid; 1326 gap:26px; 1327} 1328 1329.usp-list.variant-soft { 1330 grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); 1331} 1332 1333.usp-item { 1334 display:flex; 1335 gap:16px; 1336 align-items:flex-start; 1337 background:#fff; 1338 padding:18px 18px 18px 16px; 1339 border:1px solid rgba(226,226,226,0.7); 1340 border-radius:18px; 1341 position:relative; 1342 overflow:hidden; 1343 isolation:isolate; 1344 box-shadow:0 2px 4px -1px rgba(0,0,0,.04), 0 6px 18px -6px rgba(0,0,0,.08); 1345 transition: .35s cubic-bezier(.16,.8,.32,1); 1346} 1347 1348.usp-item::before { 1349 content:""; 1350 position:absolute; 1351 inset:0; 1352 background:radial-gradient(circle at 18% 22%, rgba(242,140,26,.18), transparent 60%); 1353 opacity:.65; 1354 transition:.5s; 1355 z-index:-1; 1356} 1357 1358.usp-item:hover { 1359 transform:translateY(-6px); 1360 box-shadow:0 10px 26px -8px rgba(0,0,0,.15), 0 3px 8px -2px rgba(0,0,0,.08); 1361 border-color:rgba(242,140,26,.35); 1362} 1363 1364.usp-item:hover::before { 1365 background:radial-gradient(circle at 70% 40%, rgba(242,140,26,.22), transparent 65%); 1366} 1367 1368.usp-icon { 1369 flex:0 0 56px; 1370 width:56px; 1371 height:56px; 1372 border-radius:16px; 1373 display:flex; 1374 align-items:center; 1375 justify-content:center; 1376 background: 1377 linear-gradient(135deg,#FFE7CC,#FFD9A8); 1378 position:relative; 1379 box-shadow:inset 0 0 0 1px rgba(255,255,255,.6), 0 4px 10px -3px rgba(0,0,0,.15); 1380 transition:.45s; 1381} 1382 1383.usp-item:hover .usp-icon { 1384 transform:rotate(-4deg) scale(1.06); 1385 box-shadow:inset 0 0 0 1px rgba(255,255,255,.8), 0 6px 14px -4px rgba(0,0,0,.20); 1386} 1387 1388.usp-icon svg { 1389 width:28px; 1390 height:28px; 1391 display:block; 1392 filter:drop-shadow(0 2px 2px rgba(0,0,0,.18));
1393} 1394 1395.usp-text h3 { 1396 font-size:15px; 1397 font-weight:600; 1398 margin:2px 0 6px; 1399 letter-spacing:.2px; 1400} 1401 1402.usp-text p { 1403 font-size:13px; 1404 line-height:1.3; 1405 margin:0; 1406 color:var(--gray-600); 1407} 1408 1409@media (max-width:680px) { 1410 .usp-section { padding: 5px 0 5px; margin-bottom: 0.75rem;} 1411 .usp-item { padding:16px 14px; } 1412 .usp-icon { width:50px;height:50px;border-radius:14px; } 1413 .usp-text h3 { font-size:14px; margin-bottom:4px; } 1414 .usp-text p { font-size:12.5px; } 1415 .usp-list { gap: 10px; } 1416} 1417 1418.flash { padding:50px 0 30px; background:#fff; } 1419.flash-head { 1420 display:flex; align-items:center; 1421 justify-content:space-between; 1422 margin-bottom:20px; 1423} 1424.flash-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1425.flash .countdown { 1426 font-family:monospace; 1427 background:#111; 1428 color:#fff; 1429 padding:8px 14px; 1430 border-radius:8px; 1431 font-size:16px; 1432 letter-spacing:1px; 1433} 1434.flash-scroll { 1435 display:flex; 1436 gap:16px; 1437 overflow-x:auto; 1438 padding-bottom:6px; 1439 scrollbar-width:none; 1440} 1441.flash-scroll::-webkit-scrollbar { display:none; } 1442 1443.products { padding:60px 0; background:#fff; } 1444.products-head { 1445 display:flex; justify-content:space-between; align-items:center; 1446 margin-bottom:24px; 1447 flex-wrap:wrap; 1448 gap:16px; 1449} 1450.products-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1451.product-filters input { 1452 padding:8px 12px; 1453 border:1px solid var(--border); 1454 border-radius:8px; 1455 font-size:14px; 1456 width:220px; 1457} 1458 1459.product-grid { 1460 display:grid; 1461 gap:24px; 1462 grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); 1463} 1464 1465.prod-card { 1466 background:#fff; 1467 border:1px solid var(--border); 1468 padding:12px; 1469 border-radius:12px; 1470 position:relative; 1471 display:flex; 1472 flex-direction:column; 1473 transition:var(--transition); 1474} 1475.prod-card.small { min-width:160px; } 1476.prod-card:hover { box-shadow:0 8px 20px rgba(0,0,0,.12); transform:translateY(-3px); } 1477.prod-card .img-box { 1478 background:var(--gray-100); 1479 border-radius:10px; 1480 aspect-ratio:1/1; 1481 display:flex; 1482 align-items:center; 1483 justify-content:center; 1484 overflow:hidden; 1485} 1486.prod-card .img-box img { width:100%; height:100%; object-fit:contain; transition:var(--transition); } 1487.prod-card:hover .img-box img { transform:scale(1.04); } 1488.prod-card .name { 1489 font-size:13.5px; 1490 font-weight:500; 1491 margin:10px 0 6px; 1492 min-height:36px; 1493 line-height:1.3; 1494} 1495.price { 1496 display:flex; 1497 gap:6px; 1498 align-items:center; 1499 flex-wrap:wrap; 1500} 1501.price strong { color:var(--brand-600); font-size:14px; } 1502.price .old { 1503 font-size:11px; 1504 color:var(--gray-500); 1505 text-decoration:line-through; 1506} 1507.tag.discount { 1508 position:absolute; 1509 top:8px; left:8px; 1510 background:#E54848; 1511 color:#fff; 1512 font-size:11px; 1513 padding:4px 6px; 1514 border-radius:6px; 1515 font-weight:600; 1516} 1517.btn { 1518 border:none; 1519 cursor:pointer; 1520 font-weight:500; 1521 font-size:14px; 1522 padding:12px 22px; 1523 border-radius:10px; 1524 background:var(--gray-900); 1525 color:#fff; 1526 transition:var(--transition); 1527 text-decoration:none; 1528 text-align:center; 1529 display:inline-flex; 1530 align-items:center; 1531 justify-content:center; 1532 gap:6px; 1533} 1534.btn:hover { background:#000; } 1535.btn-primary { background:var(--brand-500); } 1536.btn-primary:hover { background:var(--brand-600); } 1537.btn-outline { 1538 background:#fff; 1539 color:var(--gray-900); 1540 border:1px solid var(--border); 1541} 1542.btn-outline:hover { background:var(--gray-100); } 1543.btn-sm { padding:8px 14px; font-size:13px; } 1544.btn-mini { padding:6px 10px; font-size:12px; } 1545.add-cart { margin-top:auto; background:var(--brand-500); color:#fff; } 1546.add-cart:hover { background:var(--brand-600); } 1547 1548/* ==== FOOTER COMPACT FIX PATCH (final) ==== */ 1549 1550/* 1. Pastikan grid aktif */ 1551 1552.site-footer.footer-compact .ft-grid { 1553 display:grid; 1554 grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); 1555 gap:30px 40px; 1556 align-items:start; 1557} 1558 1559/* 2. Spacing atas bawah dipadatkan */ 1560.site-footer.footer-compact .ft-top { 1561 padding:42px 0 26px; 1562} 1563 1564/* 3. Judul & teks */ 1565.site-footer.footer-compact .ft-title { 1566 margin:0 0 14px; 1567 font-size:14px; 1568 letter-spacing:.8px; 1569 text-transform:uppercase; 1570 font-weight:600;
1571 color:#82b1c9; 1572} 1573 1574.site-footer.footer-compact .ft-desc, 1575.site-footer.footer-compact .ft-text { 1576 margin:0 0 16px; 1577 font-size:13.5px; 1578 line-height:1.45; 1579 color:#c6d6e0; 1580 max-width:330px; 1581} 1582.site-footer.footer-compact .ft-desc.sm-gap { margin-top:18px; } 1583 1584/* 4. Reset ikon SVG agar tidak membesar */ 1585.site-footer.footer-compact svg { 1586 width:20px; 1587 height:20px; 1588 flex:0 0 20px; 1589 display:inline-block; 1590} 1591.site-footer.footer-compact .ft-social svg { width:18px; height:18px; } 1592.site-footer.footer-compact .ft-wa-btn svg { width:18px; height:18px; } 1593 1594/* Jika ada aturan global svg { width:100%; } â paksa override */ 1595svg { max-width:100%; } 1596.site-footer.footer-compact svg { 1597 width:20px !important; 1598 height:20px !important; 1599} 1600 1601/* 5. Link list */ 1602.site-footer.footer-compact .ft-links { 1603 list-style:none; 1604 margin:0 0 12px; 1605 padding:0; 1606 display:flex; 1607 flex-direction:column; 1608 gap:4px; 1609} 1610.site-footer.footer-compact .ft-links li { margin:0; } 1611.site-footer.footer-compact .ft-links a { 1612 display:flex; 1613 align-items:center; 1614 gap:8px; 1615 font-size:13.5px; 1616 padding:5px 2px; 1617 text-decoration:none; 1618 color:#c6d6e0; 1619 border-radius:8px; 1620 transition:.25s; 1621} 1622.site-footer.footer-compact .ft-links a:hover { 1623 background:rgba(255,255,255,.06); 1624 padding-left:6px; 1625 color:#fff; 1626} 1627 1628.site-footer.footer-compact .ico-s { 1629 width:24px; 1630 height:24px; 1631 background:#103346; 1632 color:#89b8cf; 1633 border-radius:6px; 1634 display:flex; 1635 align-items:center; 1636 justify-content:center; 1637 flex:0 0 24px; 1638 font-size:12px; 1639 transition:.3s; 1640} 1641.site-footer.footer-compact .ft-links a:hover .ico-s { 1642 background:#F28C1A; 1643 color:#fff; 1644 transform:rotate(-4deg); 1645} 1646 1647/* 6. WhatsApp button */ 1648.site-footer.footer-compact .ft-wa-btn { 1649 display:inline-flex; 1650 align-items:center; 1651 gap:8px; 1652 background:#fff; 1653 color:#062437; 1654 padding:8px 18px; 1655 font-size:13.5px; 1656 border-radius:22px;
1657 font-weight:500; 1658 text-decoration:none; 1659 box-shadow:0 3px 8px -3px rgba(0,0,0,.45); 1660 transition:.25s; 1661} 1662.site-footer.footer-compact .ft-wa-btn:hover { 1663 transform:translateY(-2px); 1664 box-shadow:0 6px 14px -6px rgba(0,0,0,.55); 1665} 1666 1667/* 7. Sosial */ 1668.site-footer.footer-compact .ft-social { 1669 display:flex; 1670 gap:12px; 1671 margin-top:4px; 1672} 1673.site-footer.footer-compact .ft-social a { 1674 width:34px; 1675 height:34px; 1676 background:rgba(255,255,255,.08); 1677 display:flex; 1678 align-items:center; 1679 justify-content:center; 1680 border-radius:10px; 1681 color:#c6d6e0; 1682 transition:.25s; 1683} 1684.site-footer.footer-compact .ft-social a:hover { 1685 background:#F28C1A; 1686 color:#fff; 1687} 1688 1689/* 8. Alamat */ 1690.site-footer.footer-compact .ft-address { 1691 font-size:12.5px; 1692 line-height:1.44; 1693 color:#8fa4b1; 1694 max-width:330px; 1695 margin-top:12px; 1696} 1697.site-footer.footer-compact .ft-address .company { 1698 margin:0 0 4px; 1699 font-weight:600; 1700 color:#e5f2f7; 1701 font-size:12.8px; 1702} 1703 1704/* 9. Payments kecil & rapi */ 1705.site-footer.footer-compact .ft-payments { 1706 margin-top:14px; 1707 display:flex; 1708 flex-direction:column; 1709 gap:8px; 1710 max-width:350px; 1711} 1712.site-footer.footer-compact .ft-payments .pay-row { 1713 display:flex; 1714 gap:8px; 1715 flex-wrap:wrap; 1716} 1717.site-footer.footer-compact .ft-payments img { 1718 width:46px; 1719 height:32px; 1720 padding:3px 5px; 1721 background:#fff; 1722 border-radius:7px; 1723 object-fit:contain; 1724 box-shadow:0 2px 6px -3px rgba(0,0,0,.45); 1725 transition:.25s; 1726} 1727.site-footer.footer-compact .ft-payments img:hover { 1728 transform:translateY(-3px); 1729 box-shadow:0 5px 12px -6px rgba(0,0,0,.55); 1730} 1731 1732/* 10. Bottom bar */ 1733.site-footer.footer-compact .ft-bottom { 1734 border-top:1px solid rgba(255,255,255,.08); 1735 background:#041f31; 1736 padding:12px 0 16px; 1737 margin-top:4px; 1738} 1739.site-footer.footer-compact .ft-copy { 1740 font-size:11.5px; 1741 color:#8fa7b5; 1742 display:flex; 1743 flex-wrap:wrap; 1744 gap:12px; 1745 justify-content:center; 1746 line-height:1.3; 1747 margin:0; 1748} 1749.site-footer.footer-compact .ft-copy span { 1750 position:relative; 1751 padding:0 10px; 1752} 1753.site-footer.footer-compact .ft-copy span+span:before { 1754 content:""; 1755 position:absolute; 1756 left:0; 1757 top:50%; 1758 width:1px; 1759 height:12px; 1760 background:rgba(255,255,255,.14); 1761 transform:translateY(-50%); 1762} 1763@media (max-width:580px){ 1764 .site-footer.footer-compact .ft-top { padding:34px 0 18px; } 1765 .site-footer.footer-compact .ft-grid { gap:26px 20px; } 1766 .site-footer.footer-compact .ft-copy span { padding:0; } 1767 .site-footer.footer-compact .ft-copy span+span:before { display:none; } 1768} 1769.copy { 1770 margin-top:40px; 1771 border-top:1px solid #222; 1772 text-align:center; 1773 padding:18px 0; 1774 font-size:12px; 1775 color:#666; 1776} 1777 1778@media (max-width:760px) { 1779 .hero-text h1 { font-size:38px; } 1780 .hero { padding:0px 0 30px; } 1781 .topbar-inner { flex-direction:column; gap:4px; padding:6px 0; } 1782 .nav-inner { flex-wrap:wrap; } 1783 .mega-cats { gap:10px; } 1784 .product-grid { gap:16px; } 1785 .prod-card { padding:10px; } 1786} 1787 1788/* === PLAZA IT DARK ORANGE FOOTER THEME === */ 1789.site-footer.footer-compact { 1790 --ft-bg-0:#051722; 1791 --ft-bg-1:#072534; 1792 --ft-bg-2:#0B2F40; 1793 --ft-bg-3:#103A4E; 1794 --ft-orange:#F28C1A; 1795 --ft-orange-dark:#D97809; 1796 --ft-text:#E9F2F6; 1797 --ft-text-sec:#B8CAD3; 1798 --ft-text-muted:#8FA4B1; 1799 --ft-divider:rgba(255,255,255,0.08); 1800 --ft-glow:rgba(242,140,26,0.18); 1801 --ft-glow-fade:rgba(242,140,26,0); 1802 color:var(--ft-text); 1803} 1804 1805/* BASE TEXT / LINKS (override sebelumnya jika perlu) */ 1806.site-footer.footer-compact .ft-title { color:#9CC5D7; } 1807.site-footer.footer-compact .ft-links a { color:var(--ft-text-sec); } 1808.site-footer.footer-compact .ft-links a:hover { color:#fff; } 1809.site-footer.footer-compact .ft-address { color:var(--ft-text-muted); } 1810.site-footer.footer-compact .ft-copy { color:#9AB2BD; } 1811 1812/* ICON BACKGROUND (kubu gelap) */ 1813.site-footer.footer-compact .ico-s { 1814 background:#0F3647; 1815 color:#9FC7DA; 1816} 1817.site-footer.footer-compact .ft-links a:hover .ico-s { 1818 background:var(--ft-orange); 1819 color:#fff; 1820} 1821 1822/* WhatsApp button biarkan putih agar kontras */ 1823.site-footer.footer-compact .ft-wa-btn { 1824 box-shadow:0 4px 12px -5px rgba(0,0,0,.6), 0 0 0 3px rgba(255,255,255,0.05); 1825} 1826.site-footer.footer-compact .ft-wa-btn:hover { 1827 box-shadow:0 8px 20px -8px rgba(0,0,0,.65), 0 0 0 3px rgba(242,140,26,0.25); 1828} 1829 1830/* Payment cards border halus */ 1831.site-footer.footer-compact .ft-payments img { 1832 box-shadow:0 2px 6px -3px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,0.05); 1833} 1834.site-footer.footer-compact .ft-payments img:hover { 1835 box-shadow:0 6px 14px -6px rgba(0,0,0,.65), 0 0 0 1px rgba(242,140,26,0.4); 1836} 1837 1838/* ========== VARIANT A: DIAGONAL EMBER (AKTIF) ========== */ 1839.site-footer.footer-compact.variant-ember { 1840 background: 1841 radial-gradient(circle at 78% 82%, var(--ft-glow) 0%, var(--ft-glow-fade) 55%), 1842 linear-gradient(140deg,var(--ft-bg-1) 0%, var(--ft-bg-2) 55%, var(--ft-bg-0) 100%); 1843} 1844.site-footer.footer-compact.variant-ember .ft-bottom { 1845 background:rgba(0,0,0,0.25); 1846 backdrop-filter:blur(2px); 1847} 1848 1849/* ========== VARIANT B: TOP BAR ORANGE ========== */ 1850.site-footer.footer-compact.variant-bar { 1851 background:var(--ft-bg-1); 1852 position:relative; 1853} 1854.site-footer.footer-compact.variant-bar::before { 1855 content:""; 1856 position:absolute; 1857 left:0;top:0; 1858 width:100%;height:3px; 1859 background:linear-gradient(90deg,var(--ft-orange-dark),var(--ft-orange)); 1860 box-shadow:0 0 12px -2px var(--ft-orange); 1861} 1862.site-footer.footer-compact.variant-bar .ft-bottom { 1863 background:#041c28; 1864} 1865 1866/* ========== VARIANT C: RADIAL GLOW BAWAH ========== */ 1867.site-footer.footer-compact.variant-radial { 1868 background: 1869 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%), 1870 linear-gradient(150deg,#052131 0%, #062d3f 50%, #041924 100%); 1871} 1872.site-footer.footer-compact.variant-radial .ft-bottom { 1873 background:#041c29; 1874} 1875
1876/* ========== VARIANT D: LAYERED STRIPES ========== */ 1877.site-footer.footer-compact.variant-stripes { 1878 background: 1879 repeating-linear-gradient(180deg, rgba(255,255,255,0.015) 0 38px, rgba(255,255,255,0) 38px 76px), 1880 linear-gradient(130deg,#052636 0%, #062a3c 60%, #041d28 100%); 1881 position:relative; 1882} 1883.site-footer.footer-compact.variant-stripes::after { 1884 content:""; 1885 position:absolute; 1886 left:0;bottom:0; 1887 width:100%;height:110px; 1888 background:linear-gradient(0deg,rgba(0,0,0,.35),rgba(0,0,0,0)); 1889 pointer-events:none; 1890} 1891.site-footer.footer-compact.variant-stripes .ft-bottom { 1892 background:#031821; 1893} 1894 1895/* Garis aksen oranye tipis di atas bottom */ 1896.site-footer.footer-compact.variant-stripes .ft-bottom { 1897 position:relative; 1898} 1899.site-footer.footer-compact.variant-stripes .ft-bottom::before { 1900 content:""; 1901 position:absolute; 1902 left:0;top:0; 1903 width:100%;height:2px; 1904 background:linear-gradient(90deg,rgba(242,140,26,0),rgba(242,140,26,.65),rgba(242,140,26,0)); 1905} 1906 1907/* ========== VARIANT E: GLASS PANEL ========== */ 1908.site-footer.footer-compact.variant-glass { 1909 background:linear-gradient(145deg,#051c29 0%, #041620 65%, #03121b 100%); 1910} 1911.site-footer.footer-compact.variant-glass .ft-grid { 1912 position:relative; 1913 padding:32px clamp(10px,2vw,24px); 1914 border-radius:22px;
1915 background:linear-gradient(140deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02)); 1916 border:1px solid rgba(255,255,255,0.08); 1917 box-shadow:0 12px 28px -12px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04) inset; 1918 backdrop-filter:blur(8px); 1919 -webkit-backdrop-filter:blur(8px); 1920} 1921.site-footer.footer-compact.variant-glass .ft-bottom { 1922 background:#02141d; 1923} 1924 1925/* HOVER ACCENTS */ 1926.site-footer.footer-compact a { 1927 transition:color .25s, background .25s, box-shadow .25s; 1928} 1929.site-footer.footer-compact .ft-links a:hover { 1930 box-shadow:0 2px 6px -3px rgba(0,0,0,.5), 1931 0 0 0 1px rgba(242,140,26,0.55); 1932} 1933.site-footer.footer-compact .ft-social a { 1934 background:#0E3445; 1935 color:#bcd4dd; 1936} 1937.site-footer.footer-compact .ft-social a:hover { 1938 background:var(--ft-orange); 1939 color:#fff; 1940 box-shadow:0 0 0 3px rgba(242,140,26,0.28); 1941} 1942 1943/* Divider di atas footer (opsional) */ 1944.footer-divider-top { 1945 height:1px; 1946 background:linear-gradient(90deg,transparent,rgba(255,255,255,0.15),transparent); 1947 width:100%; 1948 margin:0 0 4px; 1949} 1950 1951/* ==== MOBILE FOOTER COLLAPSE (Non-Homepage Only) ==== */ 1952 1953/* Desktop: wrapper tidak mengganggu grid layout */ 1954.ft-collapsible-content { 1955 display: contents; 1956} 1957 1958@media (max-width: 768px) { 1959 /* Wrapper untuk konten yang bisa collapse */ 1960 .ft-top.ft-collapsible .ft-collapsible-content { 1961 display: block; 1962 max-height: 80px; 1963 overflow: hidden; 1964 position: relative; 1965 transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1966 } 1967 1968 /* Fade gradient overlay */ 1969 .ft-top.ft-collapsible .ft-collapsible-content::after { 1970 content: ''; 1971 position: absolute; 1972 bottom: 0; 1973 left: 0; 1974 right: 0; 1975 height: 70px; 1976 background: linear-gradient(to bottom, transparent 0%, rgba(6, 43, 69, 0.6) 40%, #062b45 100%); 1977 pointer-events: none; 1978 opacity: 1; 1979 transition: opacity 0.3s ease; 1980 z-index: 1; 1981 } 1982 1983 /* State: Expanded */ 1984 .ft-top.ft-collapsible.expanded .ft-collapsible-content { 1985 max-height: 1500px; 1986 } 1987 1988 .ft-top.ft-collapsible.expanded .ft-collapsible-content::after { 1989 opacity: 0; 1990 } 1991 1992 /* Expand Button - Di luar collapsible, selalu visible */ 1993 .ft-expand-btn { 1994 display: flex;
1995 align-items: center; 1996 justify-content: center; 1997 gap: 8px; 1998 width: 100%; 1999 margin: -8px 0 0; 2000 padding: 12px 20px; 2001 background: rgba(255,255,255,0.12); 2002 border: 1px solid rgba(255,255,255,0.25); 2003 border-radius: 10px; 2004 color: #e5f2f7; 2005 font-size: 13.5px; 2006 font-weight: 500; 2007 cursor: pointer; 2008 transition: all 0.3s ease; 2009 outline: none; 2010 position: relative; 2011 z-index: 10; 2012 } 2013 2014 .ft-expand-btn:hover { 2015 background: rgba(255,255,255,0.18); 2016 border-color: rgba(255,255,255,0.35); 2017 color: #fff; 2018 transform: translateY(-2px); 2019 } 2020 2021 .ft-expand-btn:active { 2022 transform: scale(0.98); 2023 } 2024 2025 .ft-expand-btn svg { 2026 width: 18px; 2027 height: 18px; 2028 transition: transform 0.3s ease; 2029 fill: currentColor; 2030 } 2031 2032 .ft-top.ft-collapsible.expanded .ft-expand-btn { 2033 margin-top: 15px; 2034 } 2035 2036 .ft-top.ft-collapsible.expanded .ft-expand-btn svg { 2037 transform: rotate(180deg); 2038 } 2039} 2040 2041/* Desktop: hide expand button */ 2042@media (min-width: 769px) { 2043 .ft-expand-btn { 2044 display: none !important; 2045 } 2046} 2047 2048/* RESPONSIVE (jika background panel butuh penyesuaian) */ 2049@media (max-width:640px){ 2050 .site-footer.footer-compact.variant-glass .ft-grid { 2051 padding:26px 18px; 2052 } 2053} 2054 2055</style> 2056 <style data-inlined="bottom-nav.css"> 2057/* Mobile Bottom Navigation */ 2058.bottom-nav { 2059 position: fixed; 2060 bottom: 0; 2061 left: 0; 2062 right: 0; 2063 background: #ffffff; 2064 border-top: 1px solid #e5e7eb; 2065 z-index: 9998; /* Below chat widget */ 2066 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); 2067 display: none; /* Hidden on desktop */ 2068} 2069 2070.bottom-nav-inner { 2071 display: flex; 2072 justify-content: space-around; 2073 align-items: center; 2074 padding: 2px 0; 2075 max-width: 640px; 2076 margin: 0 auto; 2077} 2078 2079.bottom-nav-item { 2080 flex: 1; 2081 display: flex; 2082 flex-direction: column; 2083 align-items: center; 2084 justify-content: center; 2085 gap: 4px; 2086 padding: 8px 8px; 2087 background: none; 2088 border: none; 2089 cursor: pointer; 2090 text-decoration: none; 2091 color: #6b7280; 2092 transition: all 0.3s ease; 2093 position: relative; 2094} 2095 2096.bottom-nav-item:hover { 2097 color: #F28C1A; 2098} 2099 2100.bottom-nav-item.active { 2101 flex-direction: row; 2102 gap: 8px; 2103 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 2104 color: white; 2105 border-radius: 20px; 2106 padding: 8px 16px; 2107 box-shadow: 0 2px 8px rgba(242, 140, 26, 0.3); 2108} 2109 2110.bottom-nav-item-icon { 2111 width: 24px; 2112 height: 24px; 2113 position: relative; 2114} 2115 2116.bottom-nav-item-icon svg { 2117 width: 100%; 2118 height: 100%; 2119} 2120 2121/* Icon variant switching */ 2122.bottom-nav-item .icon-outlined { 2123 display: block; 2124} 2125 2126.bottom-nav-item .icon-filled { 2127 display: none; 2128} 2129 2130.bottom-nav-item.active .icon-outlined { 2131 display: none; 2132} 2133 2134.bottom-nav-item.active .icon-filled { 2135 display: block; 2136} 2137 2138.bottom-nav-item-label { 2139 font-size: 12px; 2140 font-weight: 600; 2141 line-height: 1; 2142 display: none; 2143 white-space: nowrap; 2144} 2145 2146.bottom-nav-item.active .bottom-nav-item-label { 2147 display: block; 2148} 2149 2150/* Avatar styles for bottom-nav */ 2151.bottom-nav-avatar-img, 2152.bottom-nav-avatar-initials { 2153 width: 24px; 2154 height: 24px; 2155 border-radius: 50%; 2156 display: block; 2157} 2158 2159.bottom-nav-avatar-img { 2160 object-fit: cover; 2161} 2162 2163.bottom-nav-avatar-initials { 2164 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 2165 color: white; 2166 font-size: 10px; 2167 font-weight: 700; 2168 display: flex; 2169 align-items: center; 2170 justify-content: center; 2171 line-height: 1; 2172 letter-spacing: 0.5px; 2173} 2174 2175/* Hide SVG when avatar is shown */ 2176.bottom-nav-item-icon:has(.bottom-nav-avatar-img) .bottom-nav-svg-icon, 2177.bottom-nav-item-icon:has(.bottom-nav-avatar-initials) .bottom-nav-svg-icon { 2178 display: none; 2179} 2180 2181/* Active state adjustments for avatar */ 2182.bottom-nav-item.active .bottom-nav-avatar-initials { 2183 background: white; 2184 color: #F28C1A; 2185 border: 2px solid rgba(255, 255, 255, 0.8); 2186 box-sizing: border-box; 2187} 2188 2189.bottom-nav-item.active .bottom-nav-avatar-img { 2190 border: 2px solid rgba(255, 255, 255, 0.9); 2191 box-sizing: border-box; 2192} 2193 2194.bottom-nav-item-badge { 2195 position: absolute; 2196 top: -4px; 2197 right: -4px; 2198 background: #ef4444; 2199 color: white; 2200 border-radius: 10px; 2201 padding: 2px 5px; 2202 font-size: 10px; 2203 font-weight: 600; 2204 min-width: 18px;
2205 height: 18px; 2206 display: flex; 2207 align-items: center; 2208 justify-content: center; 2209 line-height: 1; 2210} 2211 2212/* Show on mobile only */ 2213@media (max-width: 640px) { 2214 .bottom-nav { 2215 display: block; 2216 } 2217 2218 /* Add bottom padding to body to prevent content being hidden */ 2219 body { 2220 padding-bottom: 60px; 2221 } 2222} 2223 2224/* Mobile User Sheet - Bottom Sheet for Profile Menu */ 2225.mobile-user-sheet { 2226 position: fixed; 2227 bottom: 0; 2228 left: 0; 2229 right: 0; 2230 z-index: 10000; 2231 pointer-events: none; 2232} 2233 2234.mobile-user-sheet:not([hidden]) { 2235 pointer-events: auto; 2236} 2237 2238.mobile-user-sheet-backdrop { 2239 position: fixed; 2240 inset: 0; 2241 background: rgba(0, 0, 0, 0.5); 2242 opacity: 0; 2243 transition: opacity 0.2s ease-out; 2244} 2245 2246.mobile-user-sheet.active .mobile-user-sheet-backdrop { 2247 opacity: 1; 2248} 2249 2250.mobile-user-sheet-content { 2251 position: absolute; 2252 bottom: 0; 2253 left: 0; 2254 right: 0; 2255 background: white; 2256 border-radius: 16px 16px 0 0; 2257 max-height: 80vh; 2258 overflow-y: auto; 2259 transform: translateY(100%); 2260 transition: transform 0.3s ease-out; 2261} 2262 2263.mobile-user-sheet.active .mobile-user-sheet-content { 2264 transform: translateY(0); 2265} 2266 2267.mobile-user-sheet-handle { 2268 width: 40px; 2269 height: 4px; 2270 background: #d1d5db; 2271 border-radius: 2px; 2272 margin: 12px auto 16px; 2273} 2274 2275.mobile-user-sheet-header { 2276 padding: 0 20px 16px; 2277 border-bottom: 1px solid #e5e7eb; 2278} 2279 2280.mobile-user-info { 2281 display: flex; 2282 align-items: center; 2283 gap: 12px; 2284} 2285 2286.mobile-user-avatar { 2287 width: 48px; 2288 height: 48px; 2289 background: linear-gradient(135deg, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 2290 border-radius: 50%; 2291 display: flex; 2292 align-items: center; 2293 justify-content: center; 2294 font-size: 24px; 2295 color: #D97809; 2296 font-weight: 600; 2297} 2298 2299.mobile-user-details { 2300 flex: 1; 2301} 2302 2303.mobile-user-name { 2304 font-weight: 600; 2305 font-size: 16px; 2306 color: #1f2937; 2307 margin-bottom: 4px; 2308} 2309 2310.mobile-user-email { 2311 font-size: 13px; 2312 color: #6b7280; 2313} 2314 2315.mobile-user-sheet-body { 2316 padding: 8px 0; 2317} 2318 2319.mobile-user-sheet-item { 2320 display: flex; 2321 align-items: center; 2322 gap: 12px; 2323 padding: 14px 20px; 2324 background: none; 2325 border: none; 2326 width: 100%; 2327 text-align: left; 2328 cursor: pointer; 2329 text-decoration: none; 2330 color: #374151; 2331 transition: background 0.2s; 2332} 2333 2334.mobile-user-sheet-item:hover { 2335 background: #f9fafb; 2336} 2337 2338.mobile-user-sheet-icon { 2339 font-size: 20px; 2340 width: 24px; 2341 text-align: center; 2342} 2343 2344.mobile-user-sheet-label { 2345 font-size: 15px; 2346 font-weight: 500; 2347} 2348 2349.mobile-user-sheet-divider { 2350 height: 1px; 2351 background: #e5e7eb; 2352 margin: 8px 0; 2353} 2354 2355@keyframes fadeIn { 2356 from { 2357 opacity: 0; 2358 } 2359 to { 2360 opacity: 1; 2361 } 2362} 2363 2364@keyframes slideUp { 2365 from { 2366 transform: translateY(100%); 2367 } 2368 to { 2369 transform: translateY(0); 2370 } 2371} 2372 2373/* Ensure chat widget works well with bottom nav */ 2374@media (max-width: 640px) { 2375 .chat-widget { 2376 bottom: 0 !important; /* Chat opens from bottom */ 2377 } 2378 2379 .chat-popup { 2380 /* Chat popup should be above bottom nav */ 2381 z-index: 9999; 2382 } 2383} 2384 2385</style> 2386 <style data-inlined="search-suggest.css"> 2387.search { 2388 position: relative; 2389} 2390 2391/* AI Bot Toggle Button */ 2392.ai-bot-toggle { 2393 display: flex; 2394 align-items: center; 2395 justify-content: center; 2396 background: transparent; 2397 border: none; 2398 padding: 8px; 2399 cursor: pointer; 2400 border-radius: 8px; 2401 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 2402 margin-left: 4px; 2403 flex-shrink: 0; 2404 display: none; 2405} 2406 2407.ai-bot-toggle .bot-icon { 2408 color: #9ca3af; 2409 transition: all 0.3s ease; 2410} 2411 2412.ai-bot-toggle:hover .bot-icon { 2413 color: #6b7280; 2414 transform: scale(1.1); 2415} 2416 2417.ai-bot-toggle.active .bot-icon { 2418 color: #fb923c; 2419 animation: botPulse 2s ease-in-out infinite; 2420} 2421 2422.ai-bot-toggle.active { 2423 background: rgba(251, 146, 60, 0.1); 2424} 2425 2426.ai-bot-toggle.active:hover .bot-icon { 2427 color: #f97316; 2428} 2429 2430@keyframes botPulse { 2431 0%, 100% { transform: scale(1); } 2432 50% { transform: scale(1.12); } 2433} 2434 2435/* Search Suggest Box */ 2436.search-suggest { 2437 position: absolute; 2438 top: 100%; left: 0; right: 0; 2439 background: #fff; 2440 border: 1px solid #e1e4e8; 2441 border-top: none; 2442 box-shadow: 0 4px 12px rgba(0,0,0,0.08); 2443 max-height: 400px; 2444 overflow-y: auto; 2445 z-index: 100; 2446 padding: 0; 2447 font-size: 14px; 2448 border-radius: 0 0 8px 8px; 2449} 2450 2451.suggest-item { 2452 display: flex; 2453 align-items: center; 2454 gap: 8px; 2455 padding: 10px 16px; 2456 cursor: pointer; 2457 transition: all 0.2s ease; 2458 border-bottom: 1px solid #f1f5f9; 2459} 2460 2461.suggest-item:last-child { 2462 border-bottom: none; 2463} 2464 2465.suggest-item:hover, 2466.suggest-item.active { 2467 background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); 2468 padding-left: 20px; 2469} 2470 2471.suggest-thumb { 2472 width: 42px; 2473 height: 42px; 2474 object-fit: cover; 2475 border: 1px solid #ddd; 2476 border-radius: 4px; 2477 background: #fafafa; 2478} 2479 2480.suggest-meta { flex: 1; min-width: 0; } 2481 2482.suggest-name { 2483 font-weight: 500;
2484 color: #222; 2485 line-height: 1.2; 2486 white-space: nowrap; 2487 overflow: hidden; 2488 text-overflow: ellipsis; 2489} 2490 2491.suggest-price { 2492 color: #d32f2f; 2493 font-size: 12px; 2494 margin-top: 2px; 2495} 2496 2497/* AI Insights */ 2498.suggest-ai-insight { 2499 display: flex; 2500 align-items: center; 2501 gap: 8px; 2502 padding: 8px 12px; 2503 background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); 2504 border-bottom: 1px solid #bae6fd; 2505 color: #0369a1; 2506 font-size: 12px; 2507 font-weight: 500; 2508 line-height: 1.4; 2509} 2510 2511.suggest-ai-insight svg { 2512 flex-shrink: 0; 2513 opacity: 0.8; 2514} 2515 2516.suggest-ai-insight span { 2517 flex: 1; 2518} 2519 2520/* AI Progress States */ 2521.ai-progress-container { 2522 padding: 20px 16px; 2523 background: linear-gradient(135deg, #fafbff 0%, #f5f7ff 100%); 2524} 2525 2526.ai-progress-header { 2527 display: flex; 2528 align-items: center; 2529 gap: 10px; 2530 margin-bottom: 16px; 2531} 2532 2533.ai-progress-icon { 2534 width: 36px; 2535 height: 36px; 2536 display: flex; 2537 align-items: center; 2538 justify-content: center; 2539 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 2540 border-radius: 50%; 2541 color: #fff; 2542 font-size: 18px; 2543 animation: aiIconSpin 3s linear infinite; 2544} 2545 2546@keyframes aiIconSpin { 2547 0% { transform: rotate(0deg); } 2548 100% { transform: rotate(360deg); } 2549} 2550 2551.ai-progress-text { 2552 flex: 1; 2553} 2554 2555.ai-progress-title { 2556 font-weight: 600; 2557 color: #1e293b; 2558 font-size: 14px; 2559 margin-bottom: 2px; 2560} 2561 2562.ai-progress-subtitle { 2563 font-size: 12px; 2564 color: #64748b; 2565} 2566 2567/* Progress Steps */ 2568.ai-progress-steps { 2569 display: flex; 2570 flex-direction: column; 2571 gap: 10px; 2572 margin-top: 16px; 2573} 2574 2575.ai-progress-step { 2576 display: flex; 2577 align-items: center; 2578 gap: 10px; 2579 padding: 8px 12px; 2580 background: #fff; 2581 border-radius: 8px; 2582 border: 1px solid #e2e8f0; 2583 transition: all 0.3s ease; 2584} 2585 2586.ai-progress-step.active { 2587 border-color: #667eea; 2588 background: #f8f9ff; 2589 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); 2590} 2591 2592.ai-progress-step.completed { 2593 border-color: #10b981; 2594 background: #f0fdf4; 2595} 2596 2597.ai-step-icon { 2598 width: 20px; 2599 height: 20px; 2600 display: flex; 2601 align-items: center; 2602 justify-content: center; 2603 border-radius: 50%; 2604 font-size: 11px; 2605 flex-shrink: 0; 2606} 2607 2608.ai-progress-step.active .ai-step-icon { 2609 background: #667eea; 2610 color: #fff; 2611 animation: pulse 1.5s ease-in-out infinite; 2612} 2613 2614.ai-progress-step.completed .ai-step-icon { 2615 background: #10b981; 2616 color: #fff; 2617} 2618 2619.ai-progress-step.pending .ai-step-icon { 2620 background: #e2e8f0; 2621 color: #94a3b8; 2622} 2623 2624.ai-step-text { 2625 flex: 1; 2626 font-size: 13px; 2627 color: #475569; 2628 font-weight: 500; 2629} 2630 2631.ai-progress-step.completed .ai-step-text { 2632 color: #059669; 2633} 2634 2635/* AI Analysis Result Banner */ 2636.ai-result-banner { 2637 padding: 12px 16px; 2638 background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); 2639 border-bottom: 1px solid #86efac; 2640 display: flex; 2641 align-items: center; 2642 gap: 10px; 2643} 2644 2645.ai-result-icon { 2646 font-size: 20px; 2647 animation: bounceIn 0.5s ease; 2648} 2649 2650@keyframes bounceIn { 2651 0% { transform: scale(0); opacity: 0; } 2652 50% { transform: scale(1.1); } 2653 100% { transform: scale(1); opacity: 1; } 2654} 2655 2656.ai-result-text { 2657 flex: 1; 2658} 2659 2660.ai-result-title { 2661 font-weight: 600; 2662 color: #065f46; 2663 font-size: 13px; 2664 margin-bottom: 2px; 2665} 2666 2667.ai-result-description { 2668 font-size: 12px; 2669 color: #047857; 2670 opacity: 0.9; 2671} 2672 2673/* Product Items Section Header */ 2674.suggest-section-header { 2675 padding: 10px 16px; 2676 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); 2677 border-bottom: 1px solid #e2e8f0; 2678} 2679 2680.suggest-header-main { 2681 display: flex; 2682 align-items: center; 2683 gap: 6px; 2684 margin-bottom: 4px; 2685} 2686 2687.suggest-header-icon { 2688 font-size: 14px; 2689} 2690 2691.suggest-header-text { 2692 font-size: 12px; 2693 font-weight: 600; 2694 color: #64748b; 2695 text-transform: uppercase; 2696 letter-spacing: 0.5px; 2697} 2698 2699.suggest-header-hint { 2700 font-size: 11px; 2701 color: #94a3b8; 2702 font-weight: 400; 2703 text-transform: none; 2704 letter-spacing: 0; 2705 display: flex; 2706 align-items: center; 2707 gap: 4px; 2708} 2709 2710.suggest-header-hint kbd { 2711 display: inline-block; 2712 padding: 2px 6px; 2713 font-size: 10px; 2714 font-weight: 600; 2715 line-height: 1; 2716 color: #475569; 2717 background: #fff; 2718 border: 1px solid #cbd5e1; 2719 border-radius: 4px; 2720 box-shadow: 0 1px 2px rgba(0,0,0,0.05); 2721 font-family: 'Segoe UI', monospace; 2722} 2723 2724/* Footer Hint */ 2725.suggest-footer-hint { 2726 display: flex; 2727 align-items: center; 2728 justify-content: center; 2729 padding: 12px 16px; 2730 background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); 2731 border-top: 1px solid #fbbf24; 2732 color: #92400e; 2733 font-size: 12px; 2734 font-weight: 500; 2735 text-align: center; 2736 cursor: default; 2737} 2738 2739.suggest-footer-hint svg { 2740 color: #f59e0b; 2741 flex-shrink: 0; 2742} 2743 2744.suggest-footer-hint kbd { 2745 display: inline-block; 2746 padding: 3px 7px; 2747 margin: 0 3px; 2748 font-size: 11px; 2749 font-weight: 600; 2750 line-height: 1; 2751 color: #78350f; 2752 background: #fff; 2753 border: 1px solid #f59e0b; 2754 border-radius: 4px; 2755 box-shadow: 0 1px 3px rgba(245, 158, 11, 0.2); 2756 font-family: 'Segoe UI', monospace; 2757} 2758 2759.suggest-footer-link { 2760 color: #ea580c; 2761 font-weight: 600; 2762 text-decoration: underline; 2763 text-underline-offset: 2px; 2764 text-decoration-thickness: 1px; 2765 cursor: pointer; 2766 transition: all 0.2s ease; 2767} 2768 2769.suggest-footer-link:hover { 2770 color: #c2410c; 2771 text-decoration-thickness: 2px; 2772 text-shadow: 0 0 8px rgba(234, 88, 12, 0.3); 2773} 2774 2775/* Loading state */ 2776.suggest-loading { 2777 display: flex; 2778 align-items: center; 2779 justify-content: center; 2780 gap: 8px; 2781 padding: 16px 12px; 2782 color: #64748b; 2783 font-size: 13px; 2784} 2785 2786@keyframes pulse { 2787 0%, 100% { opacity: 1; } 2788 50% { opacity: 0.5; } 2789} 2790 2791.suggest-loading { 2792 animation: pulse 1.5s ease-in-out infinite; 2793} 2794 2795/* Responsive Mobile Styles */ 2796@media (max-width: 768px) { 2797 .ai-bot-toggle { 2798 padding: 6px; 2799 margin-left: 0; 2800 } 2801 2802 .ai-bot-toggle .bot-icon {
2803 width: 20px; 2804 height: 20px; 2805 } 2806 2807 .search-icon-btn { 2808 margin-right: 4px; 2809 } 2810 2811 .search-icon-btn svg { 2812 width: 18px; 2813 height: 18px; 2814 } 2815 2816 .search-suggest { 2817 max-height: 350px; 2818 } 2819 2820 /* Mobile hint adjustments */ 2821 .suggest-header-hint { 2822 font-size: 10px; 2823 } 2824 2825 .suggest-header-hint kbd { 2826 padding: 1px 4px; 2827 font-size: 9px; 2828 } 2829 2830 .suggest-footer-hint { 2831 padding: 10px 12px; 2832 font-size: 11px; 2833 } 2834 2835 .suggest-footer-hint svg { 2836 width: 14px; 2837 height: 14px; 2838 } 2839 2840 .suggest-footer-hint kbd { 2841 padding: 2px 5px; 2842 font-size: 10px; 2843 } 2844 2845 .ai-progress-container { 2846 padding: 16px 12px; 2847 } 2848 2849 .ai-progress-icon { 2850 width: 32px; 2851 height: 32px; 2852 font-size: 16px; 2853 } 2854 2855 .ai-progress-title { 2856 font-size: 13px; 2857 } 2858 2859 .ai-progress-subtitle { 2860 font-size: 11px; 2861 } 2862 2863 .ai-progress-step { 2864 padding: 6px 10px; 2865 } 2866 2867 .ai-step-text { 2868 font-size: 12px; 2869 } 2870 2871 .suggest-item { 2872 padding: 8px 12px; 2873 } 2874 2875 .suggest-thumb { 2876 width: 36px; 2877 height: 36px; 2878 } 2879 2880 .suggest-name { 2881 font-size: 13px; 2882 } 2883 2884 .suggest-price { 2885 font-size: 11px; 2886 } 2887} 2888 2889@media (max-width: 480px) { 2890 .ai-bot-toggle { 2891 padding: 4px; 2892 } 2893 2894 .ai-bot-toggle .bot-icon { 2895 width: 18px; 2896 height: 18px; 2897 } 2898 2899 .search-icon-btn svg { 2900 width: 16px; 2901 height: 16px; 2902 } 2903 2904 /* Ultra-compact hints for small screens */ 2905 .suggest-header-hint { 2906 display: none; /* Hide header hint on very small screens */ 2907 } 2908 2909 .suggest-footer-hint { 2910 padding: 8px 10px; 2911 font-size: 10px; 2912 flex-direction: column; 2913 gap: 4px; 2914 } 2915 2916 .suggest-footer-hint svg { 2917 display: none; /* Hide icon to save space */ 2918 } 2919 2920 .suggest-footer-hint kbd { 2921 padding: 2px 4px; 2922 font-size: 9px; 2923 } 2924 2925 .ai-progress-steps { 2926 gap: 8px; 2927 } 2928 2929 .ai-step-text { 2930 font-size: 11px; 2931 } 2932} 2933 2934/* AI Search Prompt Box */ 2935.ai-search-prompt { 2936 padding: 24px; 2937 background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%); 2938} 2939 2940.ai-prompt-header { 2941 display: flex; 2942 align-items: center; 2943 gap: 12px; 2944 margin-bottom: 16px; 2945} 2946 2947.ai-prompt-icon { 2948 width: 48px; 2949 height: 48px; 2950 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 2951 border-radius: 12px; 2952 display: flex; 2953 align-items: center; 2954 justify-content: center; 2955 font-size: 24px; 2956 box-shadow: 0 4px 12px rgba(251, 146, 60, 0.3); 2957} 2958 2959.ai-prompt-text { 2960 flex: 1; 2961} 2962 2963.ai-prompt-title { 2964 font-size: 16px; 2965 font-weight: 700; 2966 color: #1f2937; 2967 margin-bottom: 4px; 2968} 2969 2970.ai-prompt-subtitle { 2971 font-size: 13px; 2972 color: #6b7280; 2973} 2974 2975.ai-prompt-query { 2976 background: white; 2977 border: 2px solid #fb923c; 2978 border-radius: 8px; 2979 padding: 12px 16px; 2980 margin-bottom: 16px; 2981} 2982 2983.ai-query-label { 2984 font-size: 11px; 2985 font-weight: 600; 2986 color: #fb923c; 2987 text-transform: uppercase; 2988 letter-spacing: 0.5px; 2989 margin-bottom: 4px; 2990} 2991 2992.ai-query-text { 2993 font-size: 15px; 2994 font-weight: 600; 2995 color: #1f2937; 2996} 2997 2998.ai-search-trigger-btn { 2999 width: 100%; 3000 padding: 14px 20px; 3001 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 3002 color: white; 3003 border: none; 3004 border-radius: 10px; 3005 font-size: 15px; 3006 font-weight: 700; 3007 cursor: pointer; 3008 display: flex; 3009 align-items: center; 3010 justify-content: center; 3011 gap: 8px; 3012 transition: all 0.3s ease; 3013 box-shadow: 0 4px 12px rgba(251, 146, 60, 0.4); 3014} 3015 3016.ai-search-trigger-btn:hover { 3017 transform: translateY(-2px); 3018 box-shadow: 0 6px 20px rgba(251, 146, 60, 0.5); 3019} 3020 3021.ai-search-trigger-btn:active { 3022 transform: translateY(0); 3023} 3024 3025.ai-btn-icon { 3026 font-size: 18px; 3027 animation: sparkle 2s ease-in-out infinite; 3028} 3029 3030@keyframes sparkle { 3031 0%, 100% { opacity: 1; transform: scale(1); } 3032 50% { opacity: 0.7; transform: scale(1.2); } 3033} 3034 3035.ai-btn-text { 3036 font-size: 15px; 3037} 3038 3039.ai-prompt-footer { 3040 margin-top: 12px; 3041 text-align: center; 3042 font-size: 12px; 3043 color: #9ca3af; 3044 font-style: italic; 3045} 3046 3047/* ==================== TRENDING SEARCH STYLES ==================== */ 3048 3049.search-suggest.trending-mode { 3050 border-top: 3px solid #fb923c; 3051 max-width: 900px; 3052 min-width: 600px; 3053} 3054 3055/* Two-column container */ 3056.trending-container { 3057 display: flex; 3058 gap: 0; 3059 padding: 0 !important; 3060} 3061 3062.trending-column { 3063 flex: 1; 3064 min-width: 0; 3065 padding: 0 !important; 3066} 3067 3068.trending-column.trending-searches { 3069 border-right: 2px solid #e2e8f0; padding: 0 !important; 3070} 3071 3072/* Mobile: stack columns vertically */ 3073@media (max-width: 768px) { 3074 .search-suggest.trending-mode { 3075 min-width: auto; 3076 max-width: 100%; 3077 } 3078 3079 .trending-container { 3080 flex-direction: column; 3081 } 3082 3083 .trending-column.trending-searches {
3084 border-right: none; 3085 border-bottom: 2px solid #e2e8f0; 3086 } 3087} 3088 3089.trending-header { 3090 display: flex; 3091 align-items: center; 3092 gap: 8px; 3093 padding: 14px 16px; 3094 border-bottom: 1px solid #e2e8f0; 3095 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 3096 color: white; 3097 position: sticky; 3098 top: 0; 3099 z-index: 10; 3100} 3101 3102.trending-icon { 3103 font-size: 20px; 3104 animation: trendingPulse 2s infinite; 3105} 3106 3107@keyframes trendingPulse { 3108 0%, 100% { 3109 transform: scale(1); 3110 filter: drop-shadow(0 0 0 rgba(251, 146, 60, 0)); 3111 } 3112 50% { 3113 transform: scale(1.15); 3114 filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8)); 3115 } 3116} 3117 3118.trending-title { 3119 font-weight: 700; 3120 font-size: 14px; 3121 letter-spacing: 0.3px; 3122} 3123 3124.trending-subtitle { 3125 font-size: 11px; 3126 opacity: 0.9; 3127 margin-left: auto; 3128 font-style: italic; 3129} 3130 3131/* Trending searches item */ 3132.trending-item { 3133 display: flex; 3134 align-items: center; 3135 gap: 12px; 3136 padding: 12px 16px; 3137 cursor: pointer; 3138 transition: all 0.2s ease; 3139 border-bottom: 1px solid #f1f5f9; 3140 position: relative; 3141} 3142 3143.trending-item:hover { 3144 background: linear-gradient(90deg, #fff7ed 0%, #ffedd5 100%); 3145 padding-left: 20px; 3146} 3147 3148.trending-item:hover::before { 3149 content: ''; 3150 position: absolute; 3151 left: 0; 3152 top: 0; 3153 bottom: 0; 3154 width: 4px; 3155 background: linear-gradient(180deg, #fb923c 0%, #f97316 100%); 3156} 3157 3158.trending-item:last-child { 3159 border-bottom: none; 3160} 3161 3162.trending-rank { 3163 font-size: 20px; 3164 flex-shrink: 0; 3165 width: 32px; 3166 text-align: center; 3167} 3168 3169.trending-query { 3170 flex: 1; 3171 font-size: 14px; 3172 color: #1f2937; 3173 font-weight: 500; 3174 line-height: 1.4; 3175} 3176 3177.trending-category { 3178 font-size: 11px; 3179 color: #f97316; 3180 background: #fff7ed; 3181 padding: 3px 10px; 3182 border-radius: 12px; 3183 white-space: nowrap; 3184 font-weight: 500; 3185 border: 1px solid #fed7aa; 3186} 3187 3188/* Trending products item */ 3189.trending-product-item { 3190 display: flex; 3191 align-items: center; 3192 gap: 12px; 3193 padding: 12px 16px; 3194 cursor: pointer; 3195 transition: all 0.2s ease; 3196 border-bottom: 1px solid #f1f5f9; 3197 position: relative; 3198} 3199 3200.trending-product-item:hover { 3201 background: linear-gradient(90deg, #fef3c7 0%, #fde68a 100%); 3202 padding-left: 20px; 3203} 3204 3205.trending-product-item:hover::before { 3206 content: ''; 3207 position: absolute; 3208 left: 0; 3209 top: 0; 3210 bottom: 0; 3211 width: 4px; 3212 background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%); 3213} 3214 3215.trending-product-item:last-child { 3216 border-bottom: none; 3217} 3218 3219.trending-product-thumb { 3220 width: 48px; 3221 height: 48px; 3222 object-fit: cover; 3223 border-radius: 8px; 3224 border: 1px solid #e5e7eb; 3225 flex-shrink: 0; 3226 background: white; 3227} 3228 3229.trending-product-info { 3230 flex: 1; 3231 min-width: 0; 3232} 3233 3234.trending-product-name { 3235 font-size: 13px; 3236 font-weight: 400; 3237 color: #1f2937; 3238 line-height: 1.4; 3239 margin-bottom: 0; 3240 padding: 0 !important; 3241 overflow: hidden; 3242 text-overflow: ellipsis; 3243 display: -webkit-box; 3244 -webkit-line-clamp: 2; 3245 -webkit-box-orient: vertical; 3246} 3247 3248.trending-product-price { 3249 font-size: 12px; 3250 font-weight: 700; 3251 color: #f59e0b; 3252 padding: 4px 0 !important; 3253} 3254.trending-product { padding: 0;} 3255/* ==================== END TRENDING STYLES ==================== */ 3256 3257</style> 3258 <style data-inlined="product-detail.css"> 3259/* === PRODUCT DETAIL v3 (Tokopedia-like) â Full stylesheet === */ 3260 3261/* Root / Container */ 3262.product-single { 3263 --pd-gap: 2rem; 3264 --pd-border: #e2e8f0; 3265 --pd-bg: #ffffff; 3266 --pd-muted: #64748b; 3267 --pd-dark: #0f172a; 3268 --pd-accent: #0d6efd; /* tema utama situs */ 3269 --pd-radius: 12px; 3270 --pd-soft: #f8fafc; 3271 --pd-soft-border: #e5e7eb; 3272 --pd-danger: #dc2626; 3273 --pd-success-bg: #dcfce7; 3274 --pd-success-text: #166534; 3275 --pd-danger-bg: #fee2e2; 3276 --pd-danger-text: #b91c1c; 3277 --pd-shadow: 0 6px 20px -10px rgba(0,0,0,.12); 3278 --pd-transition: .25s cubic-bezier(.4,.0,.2,1); 3279 max-width:1280px; 3280 margin-inline:auto; 3281} 3282 3283/* === Sticky Product Header Bar (muncul saat scroll) === */ 3284.pd-sticky-header { 3285 position: fixed; 3286 top: 0; 3287 left: 0; 3288 right: 0; 3289 z-index: 998; 3290 background: rgba(255, 255, 255, 0.98); 3291 backdrop-filter: blur(12px); 3292 -webkit-backdrop-filter: blur(12px); 3293 border-bottom: 1px solid rgba(226, 232, 240, 0.8); 3294 box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.08); 3295 transform: translateY(-100%); 3296 opacity: 0; 3297 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), 3298 opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); 3299 pointer-events: none; 3300} 3301 3302.pd-sticky-header.visible { 3303 transform: translateY(0); 3304 opacity: 1;
3305 pointer-events: auto; 3306} 3307 3308.pd-sticky-header-container { 3309 max-width: 1280px; 3310 margin: 0 auto; 3311 padding: 12px 20px; 3312 display: flex; 3313 align-items: center; 3314 gap: 16px; 3315} 3316 3317.pd-sticky-thumb { 3318 width: 48px; 3319 height: 48px; 3320 flex-shrink: 0; 3321 border-radius: 8px; 3322 border: 1px solid #e2e8f0; 3323 overflow: hidden; 3324 background: #f8fafc; 3325} 3326 3327.pd-sticky-thumb img { 3328 width: 100%; 3329 height: 100%; 3330 object-fit: cover; 3331 display: block; 3332} 3333 3334.pd-sticky-info { 3335 flex: 1; 3336 min-width: 0; 3337 display: flex; 3338 flex-direction: column; 3339 gap: 4px; 3340} 3341 3342.pd-sticky-title { 3343 font-size: 0.9rem; 3344 font-weight: 600; 3345 color: #0f172a; 3346 line-height: 1.3; 3347 white-space: nowrap; 3348 overflow: hidden; 3349 text-overflow: ellipsis; 3350 margin: 0; 3351} 3352 3353.pd-sticky-price-row { 3354 display: flex; 3355 align-items: center; 3356 gap: 8px; 3357 flex-wrap: wrap; 3358} 3359 3360.pd-sticky-price { 3361 font-size: 1.1rem; 3362 font-weight: 700; 3363 color: #0d6efd; 3364 line-height: 1; 3365} 3366 3367.pd-sticky-price-original { 3368 font-size: 0.75rem; 3369 color: #94a3b8; 3370 text-decoration: line-through; 3371 line-height: 1; 3372} 3373 3374.pd-sticky-discount { 3375 background: #dc2626; 3376 color: #fff; 3377 font-size: 0.65rem; 3378 font-weight: 600; 3379 padding: 2px 6px; 3380 border-radius: 4px; 3381 letter-spacing: 0.3px; 3382 line-height: 1; 3383} 3384 3385.pd-sticky-actions { 3386 display: flex; 3387 gap: 8px; 3388 flex-shrink: 0; 3389 align-items: center; 3390} 3391 3392.pd-sticky-share-btn { 3393 padding: 8px 16px; 3394 border-radius: 8px; 3395 border: 1px solid #e2e8f0; 3396 background: #fff; 3397 color: #64748b; 3398 cursor: pointer; 3399 transition: all 0.2s ease; 3400 display: inline-flex; 3401 align-items: center; 3402 justify-content: center; 3403 flex-shrink: 0; 3404} 3405 3406.pd-sticky-share-btn:hover { 3407 background: #f8fafc; 3408 border-color: #cbd5e1; 3409 color: #0f172a; 3410 transform: translateY(-1px); 3411} 3412 3413.pd-sticky-share-btn:active { 3414 transform: translateY(0); 3415} 3416 3417.pd-sticky-share-btn svg { 3418 display: block; 3419 width: 16px; 3420 height: 16px; 3421} 3422 3423.pd-sticky-btn-chat svg { 3424 width: 16px !important; 3425 height: 16px !important; 3426 max-width: none !important; 3427 margin-right: 4px; 3428 flex-shrink: 0; 3429} 3430 3431.pd-sticky-btn { 3432 padding: 8px 16px; 3433 border-radius: 8px; 3434 font-size: 0.85rem; 3435 font-weight: 600; 3436 cursor: pointer; 3437 transition: all 0.2s ease; 3438 border: 0; 3439 display: inline-flex; 3440 align-items: center; 3441 justify-content: center; 3442 white-space: nowrap; 3443} 3444 3445.pd-sticky-btn-cart { 3446 background: #fff; 3447 color: #0d6efd; 3448 border: 1px solid #0d6efd; 3449} 3450 3451.pd-sticky-btn-cart:hover { 3452 background: rgba(13, 110, 253, 0.06); 3453} 3454 3455.pd-sticky-btn-chat { 3456 background: #fff; 3457 color: #64748b; 3458 border: 1px solid #cbd5e1; 3459 flex: 0.8; 3460} 3461 3462.pd-sticky-btn-chat:hover { 3463 background: #f8fafc; 3464 border-color: #94a3b8; 3465 color: #475569; 3466} 3467 3468.pd-sticky-btn-buy { 3469 background: #0d6efd; 3470 color: #fff; 3471 border: 1px solid #0d6efd; 3472 box-shadow: 0 2px 8px -2px rgba(13, 110, 253, 0.4); 3473} 3474 3475.pd-sticky-btn-buy:hover { 3476 filter: brightness(0.95); 3477} 3478 3479.pd-sticky-btn:disabled { 3480 opacity: 0.5; 3481 cursor: not-allowed; 3482} 3483 3484/* Hide on mobile (use existing FAB instead) */ 3485@media (max-width: 880px) { 3486 .pd-sticky-header { 3487 display: none; 3488 } 3489} 3490 3491/* === PRINT STYLES (for PDF Export) === */ 3492@media print { 3493 /* Reset & Base */ 3494 * { 3495 -webkit-print-color-adjust: exact !important; 3496 print-color-adjust: exact !important; 3497 } 3498 3499 body { 3500 background: white !important; 3501 margin: 0; 3502 padding: 0; 3503 } 3504 3505 /* Hide all navigation, buttons, and interactive elements */ 3506 nav, header, footer, 3507 nav *, header *, footer *, 3508 .navbar, .navbar *, 3509 .nav-item, .nav-link, 3510 .pd-sticky-header, 3511 .pd-sticky-header.visible, 3512 .pd-breadcrumb, 3513 .pd-sidebar, 3514 .pd-mobile-fab, 3515 .pd-share-btn, 3516 .pd-sticky-share-btn, 3517 #pd-share-btn, 3518 .share-button, 3519 [data-share-btn], 3520 button, 3521 .btn-add-cart, 3522 .btn-buy, 3523 .pd-actions, 3524 .pd-compare-section, 3525 .pd-compare-btn, 3526 .pd-compare-info, 3527 #pd-compare-btn, 3528 .compare-note, 3529 .compare-container, 3530 .compare-dropdown, 3531 .compare-panel, 3532 .compare-wrapper, 3533 [data-compare], 3534 [id*="compare"], 3535 [class*="compare"], 3536 .pd-thumb-gallery, 3537 .pd-installment-card, 3538 .pd-installment-card2, 3539 .inst-alt-banner--compact, 3540 .qoala-banner-card, 3541 .tradein-compact-card, 3542 .pwp-deals-section, 3543 #pwp-deals-section, 3544 .pwp-trigger-badge, 3545 .pd-related-full, 3546 #pd-similar, 3547 .pd-similar-strip, 3548 .pd-scroll-top, 3549 .stock-info-btn, 3550 .stock-popover, 3551 .pd-countdown-modern, 3552 .pd-share-modal-overlay, 3553 [id*="promo-"], 3554 .bottom-nav, 3555 .pd-buy-form, 3556 .pd-buy-card, 3557 .pd-extra-meta, 3558 .pd-reviews-section, 3559 #reviews, 3560 hr, 3561 .divider, 3562 .separator { 3563 display: none !important; 3564 visibility: hidden !important; 3565 opacity: 0 !important; 3566 height: 0 !important; 3567 max-height: 0 !important; 3568 overflow: hidden !important; 3569 margin: 0 !important; 3570 padding: 0 !important; 3571 border: none !important; 3572 } 3573 3574 /* Extra aggressive reset for any remaining borders */ 3575 .pd-info-card *, 3576 .pd-meta-inline *, 3577 .pd-variants * { 3578 border: none !important; 3579 border-top: none !important; 3580 border-bottom: none !important; 3581 } 3582 3583 /* Layout restructure */ 3584 .product-single { 3585 max-width: 100% !important; 3586 margin: 0 !important; 3587 padding: 0 !important; 3588 box-shadow: none !important; 3589 } 3590 3591 /* Reset all shadows and borders for print */ 3592 * { 3593 box-shadow: none !important; 3594 } 3595 3596 .pd-top-layout { 3597 display: grid !important; 3598 grid-template-columns: 280px 1fr !important; 3599 gap: 24px !important; 3600 margin: 0 0 24px 0 !important; 3601 padding: 0 !important; 3602 page-break-inside: avoid;
3603 align-items: start !important; 3604 } 3605 3606 /* Print header with logo - centered and larger */ 3607 .product-single::before { 3608 content: "" !important; 3609 display: block !important; 3610 width: 150px !important; 3611 height: 50px !important; 3612 background: url('/assets/img/logo.png') no-repeat center !important; 3613 background-size: contain !important; 3614 margin: 0 auto 20px !important; 3615 visibility: visible !important; 3616 opacity: 1 !important; 3617 } 3618 3619 /* Media card - left column */ 3620 .pd-media-card { 3621 border: none !important; 3622 padding: 0 !important; 3623 margin: 0 !important; 3624 text-align: center; 3625 page-break-inside: avoid; 3626 } 3627 3628 /* Hide desktop image, use mobile slider for both desktop and mobile */ 3629 .pd-main-image { 3630 display: none !important; 3631 } 3632 3633 /* Mobile slider - show first image only in print */ 3634 .pd-mobile-slider { 3635 display: block !important; 3636 overflow: visible !important; 3637 } 3638 3639 .pd-mslide { 3640 display: none !important; 3641 } 3642 3643 .pd-mslide:first-child { 3644 display: block !important; 3645 border: 1px solid #e2e8f0 !important; 3646 max-width: 280px !important; 3647 margin: 0 auto !important; 3648 border-radius: 8px !important; 3649 } 3650 3651 .pd-mslide:first-child img { 3652 max-height: 280px !important; 3653 width: 100% !important; 3654 height: auto !important; 3655 object-fit: contain !important; 3656 } 3657 3658 /* Info card - right column */ 3659 .pd-info-card { 3660 border: none !important; 3661 border-top: none !important; 3662 border-bottom: none !important; 3663 padding: 0 !important; 3664 margin: 0 !important; 3665 background: white !important; 3666 box-shadow: none !important; 3667 } 3668 3669 /* Title */ 3670 .pd-title-row { 3671 display: block !important; 3672 margin-bottom: 10px !important; 3673 border: none !important; 3674 border-top: none !important; 3675 border-bottom: none !important; 3676 padding: 0 !important; 3677 box-shadow: none !important; 3678 text-decoration: none !important; 3679 } 3680 3681 /* Hide share button in print */ 3682 .pd-title-row .pd-share-btn, 3683 .pd-title-row #pd-share-btn { 3684 display: none !important; 3685 } 3686 3687 .pd-title { 3688 font-size: 13pt !important; 3689 color: #000 !important; 3690 margin: 0 0 10px 0 !important; 3691 page-break-after: avoid; 3692 font-weight: 700 !important; 3693 line-height: 1.3 !important; 3694 border: none !important; 3695 border-top: none !important; 3696 border-bottom: none !important; 3697 padding-bottom: 0 !important; 3698 box-shadow: none !important; 3699 text-decoration: none !important; 3700 } 3701 3702 /* Price and QR container - side by side */ 3703 .pd-price-qr-container { 3704 display: flex !important; 3705 gap: 16px !important; 3706 align-items: flex-start !important; 3707 margin-bottom: 14px !important; 3708 box-shadow: none !important; 3709 border: none !important; 3710 text-decoration: none !important; 3711 } 3712 3713 /* Price block - left side */ 3714 .pd-price-block { 3715 flex: 1 !important; 3716 margin: 0 !important; 3717 padding-bottom: 0 !important; 3718 border-bottom: none !important; 3719 box-shadow: none !important; 3720 } 3721 3722 .pd-price-row { 3723 margin-bottom: 8px !important; 3724 border: none !important; 3725 border-top: none !important; 3726 border-bottom: none !important; 3727 padding: 0 !important; 3728 } 3729 3730 /* Hide all empty or unnecessary elements in price row */ 3731 .pd-price-row > span:empty, 3732 .pd-price-row > .sold-inline, 3733 #pd-sold-inline { 3734 display: none !important; 3735 } 3736 3737 .pd-price-current { 3738 font-size: 18pt !important; 3739 color: #0d6efd !important; 3740 } 3741 3742 /* Show price range for variant products in print */ 3743 .pd-price-current[data-price-range]::after { 3744 content: attr(data-price-range) !important; 3745 display: block !important; 3746 font-size: 11pt !important; 3747 color: #64748b !important; 3748 margin-top: 4px !important; 3749 font-weight: normal !important; 3750 } 3751 3752 /* Hide 'Range:' prefix text */ 3753 .pd-price-current[data-price-range]::after { 3754 content: attr(data-price-range) !important; 3755 } 3756 3757 .pd-price-original { 3758 font-size: 12pt !important; 3759 color: #64748b !important; 3760 } 3761 3762 .pd-discount-badge { 3763 font-size: 10pt !important; 3764 padding: 2px 6px !important; 3765 } 3766 3767 /* QR Code section - right side beside price */ 3768 .pd-qr-print { 3769 display: block !important; 3770 flex: 0 0 100px !important; 3771 margin: 0 !important; 3772 padding: 8px !important; 3773 background: #f8fafc !important; 3774 border-radius: 8px !important; 3775 text-align: center !important; 3776 page-break-inside: avoid !important; 3777 border: 1px solid #cbd5e1 !important; 3778 } 3779 3780 .pd-qr-label { 3781 font-size: 6.5pt !important; 3782 color: #64748b !important; 3783 margin-bottom: 5px !important; 3784 line-height: 1.2 !important; 3785 font-weight: 600 !important; 3786 } 3787 3788 .pd-qr-image { 3789 width: 80px !important; 3790 height: 80px !important; 3791 display: block !important; 3792 margin: 0 auto !important; 3793 } 3794 3795 /* Meta info */ 3796 .pd-meta-inline { 3797 display: flex !important; 3798 gap: 12px !important; 3799 margin-bottom: 0 !important; 3800 padding-bottom: 14px !important; 3801 font-size: 10pt !important; 3802 border: none !important; 3803 border-top: none !important; 3804 border-bottom: none !important; 3805 padding-top: 0 !important; 3806 box-shadow: none !important; 3807 text-decoration: none !important; 3808 background: transparent !important; 3809 } 3810 3811 /* Hide stock badge in print */ 3812 .stock-badge, 3813 .stock-info-btn { 3814 display: none !important; 3815 } 3816 3817 .sku { 3818 color: #64748b !important; 3819 } 3820 3821 /* Hide sold badge if empty/zero */ 3822 .sold-inline { 3823 display: none !important; 3824 } 3825 3826 /* Hide variant summary section in print */ 3827 .pd-variant-combined, 3828 #pd-variant-combined, 3829 .pd-variant-summary, 3830 [data-role="sold-chip"] { 3831 display: none !important; 3832 } 3833 3834 /* Hide compare section completely in print */ 3835 .pd-compare-section, 3836 .pd-compare-btn, 3837 #pd-compare-btn, 3838 .pd-compare-info, 3839 .compare-note, 3840 .compare-text, 3841 .compare-remove { 3842 display: none !important; 3843 visibility: hidden !important; 3844 height: 0 !important; 3845 margin: 0 !important; 3846 padding: 0 !important; 3847 overflow: hidden !important; 3848 } 3849 3850 /* Variants - show all options with selected highlighted */ 3851 .pd-variants { 3852 margin: 0 !important; 3853 padding: 0 !important; 3854 page-break-inside: avoid !important; 3855 border: none !important; 3856 border-top: none !important; 3857 border-bottom: none !important; 3858 box-shadow: none !important; 3859 background: transparent !important; 3860 } 3861 3862 /* Add 'Variant:' label before variant groups */ 3863 .pd-variants::before { 3864 content: "Variant:" !important; 3865 display: block !important; 3866 font-size: 11pt !important; 3867 font-weight: 700 !important; 3868 color: #0f172a !important; 3869 margin-bottom: 10px !important; 3870 border: none !important; 3871 padding: 0 !important; 3872 border-top: none !important; 3873 border-bottom: none !important; 3874 } 3875 3876 .v-group { 3877 display: flex !important;
3878 align-items: center !important; 3879 gap: 8px !important; 3880 margin-bottom: 8px !important; 3881 line-height: 1 !important; 3882 page-break-inside: avoid !important; 3883 border: none !important; 3884 padding: 0 !important; 3885 } 3886 3887 .v-group:last-child { 3888 margin-bottom: 0 !important; 3889 } 3890 3891 .v-label { 3892 font-weight: 700 !important; 3893 margin: 0 !important; 3894 font-size: 9pt !important; 3895 color: #475569 !important; 3896 min-width: 60px !important; 3897 flex-shrink: 0 !important; 3898 border: none !important; 3899 padding: 0 !important; 3900 } 3901 3902 .v-options { 3903 display: flex !important; 3904 gap: 6px !important; 3905 flex-wrap: wrap !important; 3906 border: none !important; 3907 padding: 0 !important; 3908 margin: 0 !important; 3909 } 3910 3911 .v-chip { 3912 border: 1px solid #cbd5e1 !important; 3913 padding: 3px 10px !important; 3914 font-size: 9pt !important; 3915 background: #f8fafc !important; 3916 border-radius: 6px !important; 3917 display: inline-flex !important; 3918 align-items: center !important; 3919 page-break-inside: avoid !important; 3920 } 3921 3922 /* Highlight selected variant with blue background */ 3923 .v-chip.selected { 3924 background: #eff6ff !important; 3925 border-color: #2563eb !important; 3926 font-weight: 600 !important; 3927 } 3928 3929 /* Hide radio inputs */ 3930 .v-chip input[type="radio"] { 3931 display: none !important; 3932 } 3933 3934 /* Hide "Habis" badge in print */ 3935 .v-chip .chip-badge { 3936 display: none !important; 3937 } 3938 3939 /* Short description - HIDDEN in print */ 3940 .pd-short-desc { 3941 display: none !important; 3942 } 3943 3944 /* QR Code section - right side beside price */ 3945 .pd-qr-print { 3946 display: block !important; 3947 flex: 0 0 100px !important; 3948 margin: 0 !important; 3949 padding: 8px !important; 3950 background: #f8fafc !important; 3951 border-radius: 8px !important; 3952 text-align: center !important; 3953 page-break-inside: avoid !important; 3954 border: 1px solid #cbd5e1 !important; 3955 } 3956 3957 .pd-qr-label { 3958 font-size: 6.5pt !important; 3959 color: #64748b !important; 3960 margin-bottom: 5px !important; 3961 line-height: 1.2 !important; 3962 font-weight: 600 !important; 3963 } 3964 3965 .pd-qr-image { 3966 width: 80px !important; 3967 height: 80px !important; 3968 display: block !important; 3969 margin: 0 auto !important; 3970 } 3971 3972 /* Full description - below 2 column layout */ 3973 .pd-full-description { 3974 border: none !important; 3975 padding: 0 !important; 3976 margin: 24px 0 0 0 !important; 3977 page-break-before: avoid; 3978 grid-column: 1 / -1 !important; 3979 } 3980 3981 .pd-full-desc-title { 3982 font-size: 13pt !important; 3983 font-weight: 700 !important; 3984 margin: 0 0 12px 0 !important; 3985 color: #000 !important; 3986 border-top: none !important; 3987 border-bottom: none !important; 3988 padding-top: 16px !important; 3989 } 3990 3991 .pd-full-desc-body { 3992 font-size: 10pt !important; 3993 line-height: 1.6 !important; 3994 color: #334155 !important; 3995 } 3996 3997 .pd-full-desc-body img { 3998 max-height: 200px !important; 3999 width: auto !important; 4000 page-break-inside: avoid; 4001 display: none !important; /* Hide images in print to save space */ 4002 } 4003 4004 /* Also target #deskripsi img if exists */ 4005 #deskripsi img { 4006 max-width: 350px !important; 4007 height: auto !important; 4008 } 4009 4010 .pd-full-desc-body table { 4011 border-collapse: collapse !important; 4012 width: 100% !important; 4013 margin: 12px 0 !important; 4014 font-size: 9pt !important; 4015 } 4016 4017 .pd-full-desc-body th, 4018 .pd-full-desc-body td { 4019 border: 1px solid #cbd5e1 !important; 4020 padding: 6px 8px !important; 4021 } 4022 4023 /* Print footer */ 4024 .product-single::after { 4025 content: "Plaza IT - plazait.co.id | Printed: " attr(data-print-date); 4026 display: block; 4027 margin-top: 30px; 4028 padding-top: 16px; 4029 border-top: 1px solid #e2e8f0; 4030 font-size: 9pt; 4031 color: #64748b; 4032 text-align: center; 4033 } 4034 4035 /* Page breaks */ 4036 .pd-media-card, 4037 .pd-price-block, 4038 .pd-full-desc-title { 4039 page-break-after: avoid; 4040 } 4041 4042 .pd-full-description { 4043 page-break-before: auto; 4044 } 4045} 4046 4047/* === Breadcrumb (truncate last item) === */ 4048.pd-breadcrumb{ 4049 display:flex; 4050 flex-wrap:nowrap; 4051 align-items:center; 4052 gap:.5rem; 4053 font-size:.72rem; 4054 margin:.7rem 0 1.2rem .5rem; 4055 color:var(--pd-muted); 4056 overflow:hidden; 4057} 4058.pd-breadcrumb a{color:var(--pd-accent);text-decoration:none} 4059.pd-breadcrumb a:hover{text-decoration:underline} 4060.pd-breadcrumb a, 4061.pd-breadcrumb span{flex:0 0 auto} 4062.pd-breadcrumb .current{ 4063 flex:1 1 auto; 4064 min-width:0; 4065 white-space:nowrap; 4066 overflow:hidden; 4067 text-overflow:ellipsis; 4068 font-weight:600;
4069 color:var(--pd-dark); 4070 padding-right:2px; 4071} 4072 4073/* === Top layout === */ 4074.pd-top-layout{ 4075 display:grid; 4076 gap:var(--pd-gap); 4077 grid-template-columns:minmax(260px,340px) minmax(460px,1fr) 300px; 4078 align-items:start; 4079 margin-bottom:2.4rem; 4080} 4081 4082/* === Media column === */ 4083.pd-media-card{ 4084 background:#fff; 4085 border:1px solid var(--pd-border); 4086 border-radius:var(--pd-radius); 4087 padding:1rem 1rem 1.2rem; 4088} 4089.pd-main-image{margin:0 0 .8rem;border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff;position:relative} 4090.pd-main-image img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4091 4092/* Gallery Thumbnails */ 4093.pd-thumb-gallery{margin-top:.6rem;display:flex;flex-wrap:wrap;gap:.55rem} 4094.pd-thumb-btn{ 4095 border:1px solid var(--pd-soft-border); 4096 background:#fff; 4097 cursor:pointer; 4098 width:64px;height:64px;padding:2px; 4099 display:flex;align-items:center;justify-content:center; 4100 overflow:hidden;border-radius:8px; 4101 transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease; 4102 position:relative; 4103} 4104.pd-thumb-btn img{max-width:100%;max-height:100%;object-fit:contain;display:block} 4105.pd-thumb-btn:hover{border-color:var(--pd-accent)} 4106.pd-thumb-btn.active{border-color:var(--pd-accent);box-shadow:0 0 0 2px rgba(13,110,253,.25)} 4107.pd-thumb-btn:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4108.pd-thumb-btn:active{transform:scale(.96)} 4109 4110/* === Info column === */ 4111.pd-info-card{ 4112 background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4113 padding:1.4rem 1.6rem 2rem; 4114} 4115.pd-title{margin:0 0 1rem;font-size:1.2rem;line-height:1.25;font-weight:600;color:var(--pd-dark)} 4116.pd-price-block{margin:0 0 1rem} 4117.pd-price-row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;margin:0 0 .35rem} 4118.pd-price-current{font-size:2rem;font-weight:700;color:var(--pd-accent)} 4119.pd-price-original{font-size:.9rem;text-decoration:line-through;color:#94a3b8} 4120.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} 4121.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} 4122.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} 4123.stock-badge.in{background:var(--pd-success-bg);color:var(--pd-success-text)} 4124.stock-badge.out{background:var(--pd-danger-bg);color:var(--pd-danger-text)} 4125.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} 4126.sku{color:var(--pd-muted)} 4127.pd-short-desc{font-size:.85rem;line-height:1.5;margin:0 0 1.2rem;color:#334155;overflow-wrap:anywhere} 4128.pd-short-desc p{margin:0 0 .75rem} 4129.pd-short-desc ul,.pd-short-desc ol{margin:.5rem 0 .9rem 1.1rem;font-size:.85rem;line-height:1.45} 4130 4131/* Meta row for sold and rating badges */ 4132.pd-meta-row { 4133 display: flex; 4134 align-items: center; 4135 flex-wrap: wrap; 4136 gap: 5px; 4137 margin-top: 8px; 4138 margin-bottom: 4px; 4139} 4140 4141/* Sales (Terjual) badge inline */ 4142.sold-inline{ 4143 display:inline-block;font-size:.65rem;font-weight:600;color:#475569; 4144 background:#f1f5f9;padding:3px 8px 4px;border-radius:6px;letter-spacing:.4px;margin-left:0; 4145} 4146 4147/* Price row divider */ 4148.price-divider { 4149 display: inline-block; 4150 margin: 0 .4rem; 4151 color: #cbd5e1; 4152 font-weight: 400; 4153} 4154 4155/* Rating badge inline */ 4156.rating-inline { 4157 display: inline-flex; 4158 align-items: center; 4159 gap: 4px; 4160 padding: 3px 8px 4px; 4161 background: #fff7ed; 4162 border-radius: 6px; 4163 font-size: .65rem; 4164 font-weight: 600; 4165 color: #ea580c; 4166 text-decoration: none; 4167 transition: all .2s ease; 4168 cursor: pointer; 4169 margin-left: .5rem; 4170 white-space: nowrap; 4171} 4172 4173.rating-inline:hover { 4174 background: #ffedd5; 4175 transform: translateY(-1px); 4176 box-shadow: 0 2px 4px rgba(234, 88, 12, 0.1); 4177} 4178 4179.rating-inline .star-icon { 4180 font-size: .75rem; 4181 color: #f59e0b; 4182 line-height: 1; 4183} 4184 4185.rating-inline .rating-value { 4186 color: #ea580c; 4187} 4188 4189.rating-inline .rating-badge-count { 4190 color: #9a3412; 4191 opacity: 0.85; 4192} 4193 4194@media (max-width: 640px) { 4195 .rating-inline { 4196 font-size: .65rem; 4197 padding: 3px 8px 4px; 4198 } 4199 .rating-inline .star-icon { 4200 font-size: .7rem; 4201 } 4202} 4203 4204/* Highlight animation for review section */ 4205@keyframes highlightFlash { 4206 0%, 100% { background-color: transparent; } 4207 50% { background-color: rgba(251, 146, 60, 0.1); } 4208} 4209 4210.highlight-flash { 4211 animation: highlightFlash 1.5s ease; 4212} 4213 4214/* === Variant chips === */ 4215.pd-variants{margin-top:14px} 4216.v-group{margin-bottom:12px} 4217.v-label{font-weight:600;margin-bottom:6px} 4218.v-options{display:flex;flex-wrap:wrap;gap:8px} 4219.v-chip{ 4220 display:inline-flex;align-items:center;gap:8px; 4221 padding:6px 10px;border:1px solid #d6d9de;border-radius:999px; 4222 cursor:pointer;background:#fff;transition:.15s ease;user-select:none 4223} 4224.v-chip:hover{border-color:#9aa3af;box-shadow:0 1px 0 rgba(0,0,0,.04)} 4225.v-chip.selected{border-color:#2563eb;background:#eff6ff} 4226.v-chip.disabled{opacity:.45;cursor:not-allowed} 4227.v-chip.out{border-style:dashed} 4228.v-chip input{display:none} 4229.v-chip .dot{width:14px;
4229height:14px;border-radius:50%;background:var(--dot,#999);box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)} 4230.v-chip .dot--neutral{background:#9aa3af} 4231.v-chip .txt{font-size:.95rem;white-space:nowrap} 4232.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} 4233.pd-variant-summary{margin-top:8px;color:#475569;font-size:.92rem} 4234.pd-variant-summary .vs-item{display:inline-flex;gap:4px;margin-right:10px} 4235 4236/* === Buy form (di sidebar kanan untuk desktop) === */ 4237.pd-buy-form{display:flex;flex-wrap:wrap;align-items:flex-end;gap:1rem 1.4rem;margin:0} 4238.qty-label{display:block;font-size:.6rem;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--pd-muted);margin-bottom:.35rem} 4239.pd-buy-form input[type=number]{width:90px;padding:.55rem .6rem;border:1px solid var(--pd-soft-border);border-radius:6px;font-size:.85rem} 4240.pd-actions{display:flex;gap:.8rem;flex-wrap:wrap} 4241 4242/* Tombol senada tema */ 4243.btn-add-cart,.btn-buy,.btn-chat-icon{ 4244 cursor:pointer;border:0;font-weight:700;border-radius:10px;font-size:.9rem; 4245 display:inline-flex;align-items:center;justify-content:center;gap:.4rem; 4246 min-height:44px;padding:0 1rem;transition:var(--pd-transition) 4247} 4248.btn-add-cart{background:var(--brand-500);color:#fff;box-shadow:0 2px 8px -2px rgba(13,110,253,.35)} 4249.btn-add-cart:hover{filter:brightness(.95)} 4250.btn-buy{background:#fff;color:var(--brand-500);border:2px solid var(--brand-500)} 4251.btn-buy:hover{background:rgba(13,110,253,.06)} 4252.btn-chat-icon{background:#fff;color:#fb923c;border:2px solid #fb923c;padding:0;width:44px;min-width:44px} 4253.btn-chat-icon:hover{background:rgba(251, 146, 60, 0.06);border-color:#f97316} 4254.btn-chat-icon svg{width:20px;height:20px;flex-shrink:0} 4255.btn-add-cart:disabled,.btn-buy:disabled,.btn-chat-icon:disabled{opacity:.5;cursor:not-allowed} 4256 4257/* Buy with Voucher Button - Cleaner, more compact design */ 4258.btn-buy-voucher{ 4259 cursor:pointer;border:0;font-weight:600;border-radius:10px; 4260 display:inline-flex;align-items:center;justify-content:space-between;gap:10px; 4261 min-height:44px;padding:0 14px 0 12px;transition:all 0.3s ease; 4262 background:linear-gradient(135deg, #7c3aed 0%, #9333ea 100%); 4263 color:#fff; 4264 box-shadow:0 3px 10px rgba(124,58,237,0.3); 4265 position:relative; 4266 overflow:hidden; 4267 flex:1 1 auto 4268} 4269.btn-buy-voucher:hover{ 4270 transform:translateY(-2px); 4271 box-shadow:0 5px 16px rgba(124,58,237,0.4) 4272} 4273.btn-buy-voucher:active{ 4274 transform:translateY(0); 4275 box-shadow:0 2px 6px rgba(124,58,237,0.3) 4276} 4277.btn-buy-voucher .bv-icon{ 4278 font-size:20px; 4279 line-height:1; 4280 flex-shrink:0 4281} 4282.btn-buy-voucher .bv-text{ 4283 display:flex; 4284 flex-direction:column; 4285 align-items:flex-start; 4286 gap:2px; 4287 flex:1; 4288 min-width:0; 4289 font-size:14px; 4290 line-height:1.2 4291} 4292.btn-buy-voucher .bv-text small{ 4293 font-size:10px; 4294 opacity:0.85; 4295 font-weight:500; 4296 letter-spacing:0.3px; 4297 font-family:monospace 4298} 4299.btn-buy-voucher .bv-save{ 4300 font-size:12px; 4301 font-weight:700; 4302 background:rgba(255,255,255,0.25); 4303 padding:4px 8px; 4304 border-radius:6px; 4305 white-space:nowrap; 4306 flex-shrink:0 4307} 4308 4309/* Extra meta (opsional) */ 4310.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} 4311.pd-extra-meta span{font-weight:600;color:var(--pd-muted);margin-right:.3rem} 4312.pd-extra-meta a{color:var(--pd-accent);text-decoration:none} 4313.pd-extra-meta a:hover{text-decoration:underline} 4314 4315/* === Sidebar === */ 4316.pd-sidebar{display:flex;flex-direction:column;gap:1.2rem} 4317.pd-side-card{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1rem 1.1rem 1.2rem} 4318.pd-side-card h3{margin:0 0 .7rem;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4319.pd-side-card ul{list-style:none;margin:0;padding:0;display:grid;gap:.45rem;font-size:.7rem;color:#334155} 4320.pd-side-card li{position:relative;padding-left:13px;line-height:1.25} 4321.pd-side-card li:before{content:'';width:6px;height:6px;background:var(--pd-accent);border-radius:50%;position:absolute;
4321left:0;top:.45rem} 4322.pay-logos{display:flex;gap:.4rem;flex-wrap:wrap;margin:.1rem 0 .4rem} 4323.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} 4324 4325/* Buy card hanya desktop (mobile pakai FAB) */ 4326.pd-buy-card{display:block} 4327@media (max-width:880px){ .pd-buy-card{display:none} .pd-title-row {padding-right: 0 !important;}} 4328 4329/* === Full Description === */ 4330.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} 4331.pd-full-desc-title{margin:0 0 1.1rem;font-size:1.15rem;font-weight:600;color:var(--pd-dark)} 4332.pd-full-desc-body{font-size:.9rem;line-height:1.6;color:#334155;overflow-wrap:anywhere} 4333.pd-full-desc-body p{margin:0 0 1rem} 4334.pd-full-desc-body ul,.pd-full-desc-body ol{margin:.5rem 0 1rem 1.2rem} 4335.pd-full-desc-body table{width:100%;border-collapse:collapse;margin:1rem 0 1.2rem} 4336.pd-full-desc-body table,.pd-full-desc-body th,.pd-full-desc-body td{border-bottom:1px dotted #cbd5e1;vertical-align: top} 4337.pd-full-desc-body th,.pd-full-desc-body td{padding:.45rem .55rem;text-align:left} 4338.pd-full-desc-body img{max-width:100%;height:auto;display:block;} 4339 4340/* === Related (container) === */ 4341.pd-related-full{ 4342 background:#fff;border:1px solid var(--pd-border); 4343 border-radius:var(--pd-radius);padding:1.4rem 1.6rem 1.9rem; 4344 margin:0 0 2rem; 4345} 4346.pd-related-title{margin:0 0 1.1rem;font-size:1.05rem;font-weight:600;color:var(--pd-dark)} 4347 4348/* Related Grid (dirapikan: gabung selector agar tidak duplikat) */ 4349.pd-related-grid, 4350.pd-related-grid--full{ 4351 display:grid !important; 4352 gap:16px !important; 4353 grid-template-columns:repeat(auto-fill,minmax(170px,1fr)) !important; 4354 align-items:stretch; 4355} 4356 4357/* Fallback jika grid tidak didukung */ 4358@supports not (display:grid){ 4359 .pd-related-grid, 4360 .pd-related-grid--full{display:flex !important;flex-wrap:wrap} 4361 .pd-related-grid .pd-related-card, 4362 .pd-related-grid--full .pd-related-card{flex:0 0 calc(20% - 16px);max-width:calc(20% - 16px)} 4363 @media (max-width:1100px){ 4364 .pd-related-grid .pd-related-card, 4365 .pd-related-grid--full .pd-related-card{flex:0 0 calc(25% - 16px);max-width:calc(25% - 16px)} 4366 } 4367 @media (max-width:820px){ 4368 .pd-related-grid .pd-related-card, 4369 .pd-related-grid--full .pd-related-card{flex:0 0 calc(33.333% - 16px);max-width:calc(33.333% - 16px)} 4370 } 4371 @media (max-width:580px){ 4372 .pd-related-grid .pd-related-card, 4373 .pd-related-grid--full .pd-related-card{flex:0 0 calc(50% - 16px);max-width:calc(50% - 16px)} 4374 } 4375} 4376 4377/* Related Card */ 4378.pd-related-card{ 4379 display:flex;flex-direction:column;border:1px solid var(--pd-soft-border); 4380 background:#fff;border-radius:10px;text-decoration:none;color:inherit; 4381 overflow:hidden;transition:var(--pd-transition);position:relative;min-width:0; 4382} 4383.pd-related-card:hover{transform:translateY(-4px);box-shadow:var(--pd-shadow)} 4384.pd-related-card .thumb{aspect-ratio:1/1;background:#f1f5f9;overflow:hidden;position:relative} 4385.pd-related-card img{width:100%;height:100%;object-fit:cover;display:block} 4386.mini-disc{position:absolute;top:6px;left:6px;background:#dc2626;color:#fff;font-size:.55rem;padding:3px 6px 2px;border-radius:4px;font-weight:600} 4387.pd-related-card .info{padding:.55rem .65rem .75rem;display:flex;flex-direction:column;gap:.25rem} 4388.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} 4389.pd-related-card .price{font-size:.7rem;font-weight:600;color:var(--pd-accent)} 4390.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;} 4391.pd-related-card .price .final {font-size: .82rem; font-weight: 700; color: #0d6efd;} 4392.pd-related-card .price .ori {font-size: .6rem; text-decoration: line-through; color: #94a3b8;font-weight: 500;} 4393 4394/* === Mobile slider (scroll-snap) â gambar utama tetap tampil === */ 4395.pd-mobile-slider{display:none} 4396.pd-mobile-slider img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4397 4398/* Gallery counter badge */ 4399.pd-gallery-counter{ 4400 position:absolute; 4401 bottom:12px; 4402 left:12px; 4403 background:rgba(255,255,255,0.25); 4404 color:#9E9E9E; 4405 padding:4px 10px; 4406 border-radius:10px; 4407 font-size:12px; 4408 font-weight:600; 4409 z-index:100; 4410 backdrop-filter:blur(6px); 4411 -webkit-backdrop-filter:blur(6px); 4412 font-variant-numeric:tabular-nums; 4413 pointer-events:none; 4414 display:none; 4415 box-shadow:0 1px 4px rgba(0,0,0,.15); 4416 line-height:1; 4417 border:1px solid rgba(0,0,0,0.08); 4418} 4419 4420@media (max-width:640px){ 4421 .pd-media-card{border: 0;padding: 0 1rem 0;} 4422 .pd-info-card {border: 0; padding: 0 1.3rem 0; } 4423 .pd-side-card {flex: 1 1 !important;} 4424 .pd-top-layout {gap: 0; margin-bottom: 0;} 4425 .pd-variants {margin-bottom: 1.5rem;} 4426 .pd-main-image{display:none} 4427 .pd-gallery-counter{display:block} 4428 .pd-mobile-slider-wrapper{ 4429 position:relative; 4430 margin-bottom:.6rem; 4431 } 4432 .pd-mobile-slider{ 4433 display:flex;gap:8px;overflow-x:auto; 4434 scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
4435 } 4436 .pd-mslide{ 4437 min-width:100%;flex:0 0 100%;scroll-snap-align:center; 4438 border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff 4439 } 4440 .pd-thumb-gallery{display:none} /* hide thumbnails on mobile */ 4441} 4442 4443/* === Mobile Floating Action Bar === */ 4444.pd-mobile-fab{ 4445 position:fixed; 4446 left:0; 4447 right:0; 4448 bottom:42px; /* Position above bottom-nav (60px min-height + 6px padding) */ 4449 display:none; 4450 align-items:center; 4451 gap:.75rem; 4452 padding:12px 16px; 4453 background:rgba(255,255,255,0.95); 4454 backdrop-filter:blur(20px); 4455 -webkit-backdrop-filter:blur(20px); 4456 border-top:1px solid rgba(228,231,235,0.6); 4457 box-shadow:0 -4px 16px -4px rgba(0,0,0,0.12); 4458 z-index:999; /* Below bottom-nav (1000) */ 4459 transition:bottom .3s cubic-bezier(0.4, 0.0, 0.2, 1); 4460} 4461 4462/* When bottom-nav is hidden, move FAB to bottom */ 4463body:has(.bottom-nav.hidden) .pd-mobile-fab { 4464 bottom: 0; 4465} 4466.pd-mobile-fab .mf-actions{display:flex;gap:.5rem;width:100%} 4467.mf-btn{ 4468 border-radius:10px; 4469 padding:.6rem 1rem; 4470 font-weight:600; 4471 font-size:14px; 4472 cursor:pointer; 4473 transition:all .2s; 4474 -webkit-tap-highlight-color:transparent; 4475 display:inline-flex; 4476 align-items:center; 4477 justify-content:center; 4478 gap:4px; 4479} 4480.mf-buy{ 4481 background:#fb923c; 4482 color:#fff; 4483 border:0; 4484 box-shadow:0 2px 8px rgba(251, 146, 60, 0.3); 4485 flex:1; 4486 flex-direction:column; 4487 padding:.55rem 1rem; 4488 line-height:1; 4489 min-height:44px; 4490 font-family:inherit; 4491} 4492.mf-buy .mf-buy-text{ 4493 font-size:11px; 4494 font-weight:600; 4495 opacity:0.95; 4496 font-family:inherit; 4497} 4498.mf-buy .mf-buy-price{ 4499 font-size:13px; 4500 font-weight:800; 4501 letter-spacing:-0.3px; 4502 font-family:inherit; 4503 line-height:1; 4504} 4505.mf-buy:active{transform:scale(0.97)} 4506.mf-cart{ 4507 background:#fff; 4508 color:#fb923c; 4509 border:2px solid #fb923c; 4510 width:44px; 4511 height:44px; 4512 min-height:44px; 4513 padding:0; 4514 flex-shrink:0; 4515 font-family:inherit; 4516} 4517.mf-cart:active{transform:scale(0.95)} 4518.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} 4519.mf-chat:active{transform:scale(0.95)} 4520.mf-chat svg{width:20px;height:20px;flex-shrink:0} 4521.mf-share{ 4522 background:#fff; 4523 color:#fb923c; 4524 border:2px solid #fb923c; 4525 width:44px; 4526 height:44px; 4527 padding:0; 4528 flex-shrink:0; 4529} 4530.mf-share:active{transform:scale(0.95)} 4531.mf-btn:disabled{opacity:.5;cursor:not-allowed} 4532@media (max-width:640px){ 4533 .pd-mobile-fab{display:flex} 4534 .product-single{padding-bottom:0;} /* Space for FAB + bottom-nav */ 4535} 4536 4537/* === Installment cards === */ 4538.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} 4539.pd-installment-card h3{margin:0 0 .2rem;font-size:.78rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4540 4541.inst-tabs{display:flex;gap:.4rem} 4542.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)} 4543.inst-tab.active,.inst-tab:hover{background:var(--pd-accent);color:#fff;border-color:var(--pd-accent)} 4544.inst-tab:focus{outline:2px solid var(--pd-accent);outline-offset:2px} 4545 4546.inst-tenors{display:flex;gap:.4rem;flex-wrap:wrap} 4547.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} 4548.tenor-btn.active,.tenor-btn:hover{background:#1d7df0;color:#fff;border-color:#1d7df0} 4549 4550.inst-result{display:flex;flex-direction:column;gap:.45rem;font-size:.63rem} 4551.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} 4552.inst-line.highlight{background:#1d7df0;color:#fff;border-color:#1d7df0;font-weight:600} 4553.inst-line .lbl{opacity:.8} 4554.inst-line .val{font-weight:600;letter-spacing:.3px} 4555.inst-footnote{font-size:.53rem;line-height:1.25;color:#64748b;margin-top:.2rem} 4556@media (max-width: 860px){ 4557 .pd-installment-card{order:3} 4558 .inst-tabs .inst-tab,.inst-tenors .tenor-btn{font-size:.65rem} 4559 4560 /* Voucher button responsive - make it simpler */ 4561 .btn-buy-voucher{ 4562 padding:0 12px; 4563 gap:8px 4564 } 4565 .btn-buy-voucher .bv-icon{ 4566 font-size:18px 4567 } 4568 .btn-buy-voucher .bv-text{ 4569 font-size:13px 4570 } 4571 .btn-buy-voucher .bv-text small{ 4572 font-size:9px 4573 } 4574 .btn-buy-voucher .bv-save{ 4575 font-size:11px; 4576 padding:3px 6px 4577 } 4578} 4579 4580/* Installment List v2 (eye-catching) */ 4581.pd-installment-card2{ 4582 background:linear-gradient(135deg,#ffffff 0%,#f1f7ff 65%,#e8f2ff 100%); 4583 border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4584 padding:1rem 1.05rem 1.2rem;display:flex;flex-direction:column;gap:.5rem; 4585 position:relative;overflow:hidden 4586} 4587.pd-installment-card2:before{ 4588 content:'';position:absolute;width:140px;height:140px; 4589 background:radial-gradient(circle at center,rgba(29,125,240,.18),rgba(29,125,240,0)); 4590 top:-40px;right:-40px;pointer-events:none 4591} 4592.pd-installment-card2 h3{margin:0;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:#0f2f54} 4593.pd-installment-card2 .inst-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem} 4594.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)} 4595 4596.inst-bank-tabs{display:flex;gap:.45rem} 4597.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} 4598.bank-tab.active,.bank-tab:hover{background:#1d7df0;color:#fff;border-color:#1d7df0;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4599.bank-tab:focus{outline:2px solid #1d7df0;outline-offset:2px} 4600 4601.inst-month-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem} 4602.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} 4603.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} 4604.inst-month-item:hover{transform:translateY(-2px);box-shadow:0 6px 14px -6px rgba(0,40,120,.25)} 4605.inst-month-item:hover:before{opacity:1} 4606.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)} 4607.amount{flex:1;font-size:.8rem;font-weight:600;color:#0f2f54;letter-spacing:.3px} 4608.tag{font-size:.55rem;font-weight:600;padding:.28rem .55rem;border-radius:30px;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap} 4609.tag-zero{background:#16a34a;color:#fff;box-shadow:0 2px 8px -3px rgba(16,180,90,.55)} 4610 4611.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} 4612.inst-disclaimer{font-size:.52rem;line-height:1.25;color:#64748b;margin-top:-.2rem} 4613 4614@media (max-width:860px){ 4615 .pd-installment-card2{order:3} 4616 .tenor-badge{min-width:38px;font-size:.75rem} 4617 .amount{font-size:.78rem} 4618} 4619 4620/* Alt Fintech Banner (compact) */ 4621.inst-alt-banner--compact{ 4622 background:linear-gradient(142deg,#1d7df0 0%,#3c98ff 55%,#67bdff 100%); 4623 color:#fff;border:0;padding:.95rem .95rem 1.05rem;border-radius:16px; 4624 box-shadow:0 6px 18px -6px rgba(0,70,160,.45);position:relative;overflow:hidden;min-height:auto; 4625} 4626.inst-alt-banner--compact:before, 4627.inst-alt-banner--compact:after{ 4628 content:'';position:absolute;border-radius:50%;background:rgba(255,255,255,.15);filter:blur(2px);pointer-events:none; 4629} 4630.inst-alt-banner--compact:before{width:130px;height:130px;top:-50px;right:-35px} 4631.inst-alt-banner--compact:after{width:90px;height:90px;bottom:-40px;left:-30px}
4632.inst-alt-banner--compact .iab-top{margin:0 0 .55rem;display:flex;flex-direction:column;gap:.25rem} 4633.inst-alt-banner--compact .iab-title{font-size:.74rem;font-weight:700;letter-spacing:.7px;line-height:1.15} 4634.inst-alt-banner--compact .iab-sub{font-size:.55rem;font-weight:500;opacity:.95;letter-spacing:.4px} 4635 4636.iab-logos-compact{display:flex;gap:.55rem;align-items:center;margin:0 0 .6rem;flex-wrap:nowrap;justify-content:space-between} 4637.iab-logos-compact .logo-fin{width:30%} 4638.iab-logos-compact .logo{ 4639 width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center; 4640 font-size:.62rem;font-weight:700;letter-spacing:.3px;position:relative;color:#fff; 4641 box-shadow:0 4px 10px -4px rgba(0,0,0,.35);background:rgba(255,255,255,.15); 4642 backdrop-filter:blur(4px);transition:.25s;text-shadow:0 1px 2px rgba(0,0,0,.4);user-select:none; 4643} 4644.iab-logos-compact .logo:before{ 4645 content:'';position:absolute;inset:0;border-radius:inherit; 4646 background:linear-gradient(140deg,rgba(255,255,255,.35),rgba(255,255,255,0));mix-blend-mode:overlay;opacity:.55;pointer-events:none; 4647} 4648.iab-logos-compact .logo:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 8px 18px -6px rgba(0,0,0,.45)} 4649 4650.iab-cta--compact{ 4651 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; 4652} 4653.iab-cta--compact:hover{transform:translateY(-2px);box-shadow:0 8px 18px -7px rgba(0,0,0,.45)} 4654.iab-footnote--compact{margin-top:.45rem;font-size:.46rem;opacity:.85;letter-spacing:.25px} 4655@media (max-width:860px){ 4656 .inst-alt-banner--compact{order:4;margin-top:.4rem;flex:1 1} 4657 .iab-logos-compact .logo{width:46px;height:46px} 4658} 4659 4660/* === Scroll To Top (baru) === */ 4661.pd-scroll-top{ 4662 position:fixed; 4663 right:20px; 4664 bottom:90px; 4665 width:48px; 4666 height:48px; 4667 border-radius:50%; 4668 background:linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); 4669 color:#0f2f54; 4670 border:1px solid rgba(226, 232, 240, 0.8); 4671 box-shadow:0 8px 20px -8px rgba(0,0,0,.3), 0 2px 6px rgba(0,0,0,.1); 4672 display:flex; 4673 align-items:center; 4674 justify-content:center; 4675 cursor:pointer; 4676 z-index:1105; 4677 opacity:0; 4678 transform:translateY(12px) scale(.92); 4679 transition:all .3s cubic-bezier(0.4, 0, 0.2, 1); 4680 backdrop-filter:blur(8px); 4681 -webkit-backdrop-filter:blur(8px); 4682} 4683.pd-scroll-top:hover{ 4684 box-shadow:0 12px 28px -10px rgba(0,0,0,.4), 0 4px 12px rgba(0,0,0,.15); 4685 transform:translateY(0) scale(1); 4686 background:linear-gradient(135deg, #ffffff 0%, #fefefe 100%); 4687 border-color:rgba(226, 232, 240, 1); 4688} 4689.pd-scroll-top:active{ 4690 transform:translateY(0) scale(.96); 4691} 4692.pd-scroll-top:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4693.pd-scroll-top.visible{opacity:1;transform:translateY(0) scale(1)} 4694@media (max-width:640px){ 4695 /* Dynamic positioning via JavaScript */ 4696 .pd-scroll-top{ 4697 right:14px; 4698 bottom:var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))); 4699 } 4700} 4701 4702/* === Responsive layout tweaks === */ 4703@media (max-width:1250px){ 4704 .pd-top-layout{grid-template-columns:minmax(260px,330px) 1fr} 4705 .pd-sidebar{grid-column:1 / -1;flex-direction:row;flex-wrap:wrap;gap:1rem} 4706 /* Other cards 2-per-row; buy card always takes full row to avoid height-mismatch gaps */ 4707 .pd-side-card{flex:1 1 calc(50% - .5rem)} 4708 .pd-buy-card{flex:0 0 100%} 4709} 4710@media (max-width:880px){ 4711 .pd-top-layout{grid-template-columns:1fr} 4712 .pd-sidebar{flex-direction:column; padding: 0 1rem;} 4713 .pd-title{font-size:1.2rem} 4714 .pd-price-current{font-size:1.5rem} 4715} 4716@media (max-width:640px){ 4717 /* Related: 2 kolom di mobile */ 4718 .pd-related-full{padding:1.2rem;border:0;padding-top: .6rem;margin-bottom:.6rem;} 4719 .pd-related-grid,.pd-related-grid--full{grid-template-columns:repeat(2,1fr) !important;gap:12px !important} 4720} 4721@media (max-width:560px){ 4722 .pd-actions{flex-direction:column} 4723 .btn-add-cart,.btn-buy,.btn-buy-voucher{width:100%} 4724 .pd-full-description{padding:1.2rem;border:0;padding-bottom:0;} 4725} 4726@media (max-width:380px){ 4727 .pd-related-full{padding:.9rem .7rem 1.1rem} 4728 .pd-related-grid,.pd-related-grid--full{gap:8px !important} 4729 .pd-related-card .info{padding:.4rem .48rem .55rem} 4730} 4731 4732/* === Modern Countdown (Product Detail) === */ 4733.pd-countdown-modern{ 4734 --cd-bg1:#0ea5e9; /* sky-500 */ 4735 --cd-bg2:#6366f1; /* indigo-500 */ 4736 --cd-text:#0b1220; 4737 --cd-contrast:#ffffff; 4738 --cd-shadow: 0 8px 24px -10px rgba(0,0,0,.25); 4739 --cd-border: rgba(255,255,255,.25); 4740 --cd-box: rgba(255,255,255,.65); 4741 --cd-box-text:#0f172a; 4742 --cd-danger:#ef4444; 4743 4744 display:flex; gap:.75rem; align-items:center; 4745 background:linear-gradient(135deg,var(--cd-bg1),var(--cd-bg2)); 4746 border:1px solid var(--cd-border); 4747 color:var(--cd-contrast); 4748 border-radius:14px; 4749 padding:.6rem .8rem; 4750 box-shadow:var(--cd-shadow); 4751 position:relative; 4752 overflow:hidden; 4753 margin-bottom: 1rem; 4754} 4755 4756.pd-countdown-modern .pd-cd-icon{ 4757 width:28px;height:28px;flex:0 0 28px; 4758 display:grid;place-items:center; 4759 background:rgba(255,255,255,.22); 4760 border:1px solid var(--cd-border); 4761 border-radius:10px; 4762 box-shadow: inset 0 1px 0 rgba(255,255,255,.35); 4763} 4764.pd-countdown-modern .pd-cd-icon svg{width:18px;height:18px;fill:#fff;opacity:.95} 4765
4766.pd-countdown-modern .pd-cd-body{display:flex;flex-direction:column;gap:.35rem;min-width:0} 4767 4768.pd-countdown-modern .pd-cd-label{ 4769 font-size:.72rem;font-weight:700;letter-spacing:.4px;opacity:.95;line-height:1; 4770 text-shadow:0 1px 0 rgba(0,0,0,.08); 4771} 4772 4773.pd-countdown-modern .pd-cd-time{ 4774 display:flex; align-items:end; gap:.35rem; flex-wrap:wrap; 4775 line-height:1; 4776} 4777 4778.pd-countdown-modern .unit{ 4779 display:flex; flex-direction:column; align-items:center; gap:.18rem; 4780 min-width:58px; 4781} 4782.pd-countdown-modern .num{ 4783 display:inline-flex; align-items:center; justify-content:center; 4784 min-width:58px; height:40px; padding:0 .4rem; 4785 background:var(--cd-box); 4786 color:var(--cd-box-text); 4787 border:1px solid rgba(15,23,42,.08); 4788 border-radius:10px; 4789 font: 800 1.05rem ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 4790 letter-spacing:.5px; 4791 box-shadow: 0 4px 14px -6px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.55); 4792} 4793.pd-countdown-modern .unit-label{ 4794 font-size:.62rem; font-weight:700; opacity:.9; text-shadow:0 1px 0 rgba(255,255,255,.15); 4795} 4796 4797.pd-countdown-modern .colon{ 4798 font-weight:900; font-size:1.05rem; opacity:.95; transform: translateY(-2px); 4799 animation: cdBlink 1.2s infinite steps(2, jump-none); 4800} 4801@keyframes cdBlink { 0%{opacity:1} 50%{opacity:.25} 100%{opacity:1} } 4802 4803.pd-countdown-modern .pd-cd-progress{ 4804 display:none; 4805 position:relative; 4806 height:6px; border-radius:999px; overflow:hidden; 4807 background:rgba(255,255,255,.25); 4808 border:1px solid rgba(255,255,255,.28); 4809} 4810.pd-countdown-modern .pd-cd-progress .bar{ 4811 height:100%; width:0%; background:linear-gradient(90deg,#22c55e,#a3e635); 4812 box-shadow: 0 2px 8px rgba(0,0,0,.25) inset; 4813 transition:width .35s ease; 4814} 4815 4816/* State saat hampir habis */ 4817.pd-countdown-modern.danger{ 4818 --cd-bg1:#f97316; /* orange-500 */ 4819 --cd-bg2:#ef4444; /* red-500 */ 4820} 4821 4822/* Responsive */ 4823@media (max-width:640px){ 4824 .pd-countdown-modern{ 4825 padding:.55rem .65rem; border-radius:12px; gap:.6rem; 4826 } 4827 .pd-countdown-modern .unit{min-width:52px} 4828 .pd-countdown-modern .num{min-width:52px;height:36px;font-size:.98rem} 4829} 4830 4831/* === Modern Countdown polish (shine + compact) === */ 4832.pd-countdown-modern{ 4833 position:relative; 4834} 4835.pd-countdown-modern::after{ 4836 content:""; 4837 position:absolute; inset:-1px; 4838 background: linear-gradient(120deg, rgba(255,255,255,.12), rgba(255,255,255,0) 35%), 4839 radial-gradient(120% 120% at 0% 0%, rgba(255,255,255,.08), rgba(255,255,255,0) 60%); 4840 border-radius: inherit; 4841 pointer-events:none; 4842 mix-blend-mode: screen; 4843 animation: sheen 4.5s ease-in-out infinite; 4844} 4845@keyframes sheen { 4846 0% { opacity:.35; transform: translateX(-10%) } 4847 50% { opacity:.15; transform: translateX(4%) } 4848 100% { opacity:.35; transform: translateX(-10%) } 4849} 4850 4851/* Mode compact (aktifkan dengan data-mode="compact" di elemen) */ 4852.pd-countdown-modern[data-mode="compact"]{ 4853 gap:.55rem; padding:.5rem .6rem; border-radius:12px; 4854} 4855.pd-countdown-modern[data-mode="compact"] .unit{ 4856 min-width:46px; 4857} 4858.pd-countdown-modern[data-mode="compact"] .num{ 4859 min-width:46px; height:34px; font-size:.92rem; 4860} 4861.pd-countdown-modern[data-mode="compact"] .pd-cd-label{ 4862 font-size:.66rem; 4863} 4864.pd-countdown-modern[data-mode="compact"] .pd-cd-icon{ 4865 width:24px; height:24px; border-radius:8px; 4866} 4867.pd-countdown-modern[data-mode="compact"] .pd-cd-progress{ 4868 height:5px; 4869} 4870 4871/* ============================================================ 4872 Sidebar: shared refinements for medium + tablet (641px â 1250px) 4873 Buy card = form & paylater on the same row. 4874 Alt-fintech logos compact (no over-stretching). 4875 ============================================================ */ 4876@media (min-width:641px) and (max-width:1250px) { 4877 /* Buy card: put qty/buy buttons and paylater promo inline */ 4878 .pd-buy-card { 4879 display: flex !important; 4880 flex-direction: row; 4881 flex-wrap: wrap; 4882 align-items: center; 4883 gap: .6rem .75rem; 4884 } 4885 .pd-buy-card .pd-buy-form { flex: 1 1 0; min-width: 0; } 4886 .pd-buy-card .paylater-promo { 4887 flex: 0 0 auto;
4888 align-self: center; 4889 margin: 0; 4890 max-width: 44%; 4891 } 4892 4893 /* Alt-fintech banner: tighter logos */ 4894 .iab-logos-compact { 4895 justify-content: center; 4896 gap: 1.25rem; 4897 } 4898 .iab-logos-compact .logo-fin { 4899 width: auto; 4900 max-height: 28px; 4901 object-fit: contain; 4902 } 4903} 4904 4905/* ============================================================ 4906 Tablet layout (641px â 880px) 4907 Restores 2-column grid + horizontal sidebar for mid-size screens. 4908 Overrides the generic â¤880px mobile-collapse rules above. 4909 ============================================================ */ 4910@media (min-width:641px) and (max-width:880px) { 4911 /* 2-column grid: image-card | info-card */ 4912 .pd-top-layout { 4913 grid-template-columns: minmax(260px, 40%) 1fr; 4914 gap: 1rem; 4915 } 4916 4917 /* Sidebar: full-width horizontal flex row below the two main columns */ 4918 .pd-sidebar { 4919 grid-column: 1 / -1; 4920 flex-direction: row; 4921 flex-wrap: wrap; 4922 gap: .75rem; 4923 padding: 0; 4924 } 4925 /* Banner cards fill 2-per-row */ 4926 .pd-side-card { flex: 1 1 calc(50% - .375rem); } 4927 /* Buy card: full row (tallest card â avoids white-space gap if shared with shorter banners) */ 4928 .pd-buy-card { display: block !important; flex: 0 0 100%; } 4929 4930 /* Hide installment simulator + alt-fintech banner on tablet (too much vertical space) */ 4931 .pd-installment-card2, 4932 .inst-alt-banner--compact { display: none !important; } 4933 4934 /* Restore sticky add-to-cart header (hidden at â¤880px by default) */ 4935 .pd-sticky-header { display: flex !important; } 4936 4937 /* Typography â tighten slightly to fit the narrower info column */ 4938 .pd-price-current { font-size: 1.65rem; } 4939 .pd-title { font-size: 1.1rem; } 4940 .pd-info-card { padding: 1.1rem 1.2rem 1.5rem; } 4941} 4942</style> 4943 <style data-inlined="product-grid.css"> 4944/* Product Grid (Firebird) */ 4945 4946.product-archive { 4947 max-width: 1320px; 4948 margin-inline: auto; 4949 padding: 0 0 3rem; 4950 /*font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;*/ 4951} 4952 4953.pa-toolbar { 4954 display: flex; 4955 flex-wrap: wrap; 4956 gap: 1rem; 4957 justify-content: space-between; 4958 align-items: flex-start; 4959} 4960 4961.pa-title { 4962 font-size: 1.4rem; 4963 margin: 0; 4964 font-weight: 600; 4965 line-height: 1.2; 4966 color: #0f172a; 4967} 4968 4969.pa-left { 4970 display: flex; 4971 flex-wrap: wrap; 4972 gap: .75rem; 4973 align-items: center; 4974} 4975 4976.pa-filter-badge { 4977 background: #f1f5f9; 4978 border: 1px solid #e2e8f0; 4979 padding: .45rem .6rem; 4980 border-radius: 8px; 4981 font-size: .7rem; 4982 display: flex; 4983 align-items: center; 4984 gap: .4rem; 4985 line-height: 1; 4986 color: #475569; 4987} 4988.pa-filter-badge strong { color: #0f172a; } 4989.pa-filter-badge .clear { 4990 display:inline-block; 4991 text-decoration:none; 4992 background:#e2e8f0; 4993 color:#475569; 4994 font-weight:600; 4995 padding:0 .35rem .05rem; 4996 border-radius:4px; 4997 transition:.2s; 4998 font-size:.75rem; 4999 line-height:1; 5000} 5001.pa-filter-badge .clear:hover { background:#cbd5e1; color:#1e293b; } 5002 5003.pa-right { 5004 display:flex; 5005 gap:1rem; 5006 flex-wrap:wrap; 5007 align-items:flex-start; 5008 margin-bottom: 0.5rem; 5009} 5010 5011.pa-search { 5012 display:flex; 5013 gap:.5rem; 5014 align-items:center; 5015 background:#fff; 5016 border:1px solid #e2e8f0; 5017 padding:.4rem .5rem; 5018 border-radius:8px; 5019} 5020.pa-search input[type=text] { 5021 border:0; 5022 outline:none; 5023 font-size:.8rem; 5024 padding:.35rem .4rem;
5025 min-width:200px; 5026} 5027.pa-search button { 5028 background:#0d6efd; 5029 border:0; 5030 color:#fff; 5031 font-size:.7rem; 5032 padding:.45rem .75rem; 5033 border-radius:6px; 5034 cursor:pointer; 5035 font-weight:600; 5036 letter-spacing:.3px; 5037 transition:.25s; 5038} 5039.pa-search button:hover { background:#0b5dc4; } 5040 5041.pa-sort { 5042 display:flex; 5043 gap:.4rem; 5044 align-items:center; 5045 font-size:.7rem; 5046 background:#fff; 5047 border:1px solid #e2e8f0; 5048 padding:.4rem .55rem; 5049 border-radius:8px; 5050} 5051.pa-sort select { 5052 font-size:.75rem; 5053 padding:.35rem .5rem; 5054 border:1px solid #cbd5e1; 5055 border-radius:6px; 5056 background:#fff; 5057 outline:none; 5058} 5059 5060.pa-empty { 5061 border:1px dashed #cbd5e1; 5062 padding:3rem 1rem; 5063 text-align:center; 5064 border-radius:12px; 5065 background:#fff; 5066} 5067.pa-empty p { 5068 margin:0 0 1rem; 5069 font-size:.9rem; 5070 color:#475569; 5071} 5072.btn-reset { 5073 display:inline-block; 5074 background:#0d6efd; 5075 color:#fff; 5076 font-size:.7rem; 5077 padding:.55rem 1rem; 5078 border-radius:8px; 5079 text-decoration:none; 5080 font-weight:600; 5081 letter-spacing:.4px; 5082 transition:.25s; 5083} 5084.btn-reset:hover { background:#0a58c7; } 5085 5086/* Grid */ 5087.pa-grid { 5088 display:grid; 5089 gap:1rem; 5090 grid-template-columns:repeat(auto-fill, minmax(180px,1fr)); 5091 align-items:stretch; 5092} 5093 5094.pa-card { 5095 background:#fff; 5096 border:1px solid #e2e8f0; 5097 border-radius:12px; 5098 display:flex; 5099 flex-direction:column; 5100 text-decoration:none; 5101 color:inherit; 5102 position:relative; 5103 overflow:hidden; 5104 transition:.28s cubic-bezier(.4,0,.2,1); 5105 min-width:0; 5106 padding: 0; 5107} 5108.pa-card:hover { 5109 transform:translateY(-4px); 5110 box-shadow:0 8px 22px -8px rgba(0,0,0,.15); 5111 border-color:#dce2ea; 5112} 5113 5114.pa-card .thumb { 5115 aspect-ratio:1/1; 5116 background:#f1f5f9; 5117 display:flex; 5118 align-items:center; 5119 justify-content:center; 5120 overflow:hidden; 5121 position:relative; 5122} 5123.pa-card img { 5124 width:100%; 5125 height:100%; 5126 object-fit:cover; 5127 display:block; 5128} 5129.badge-disc { 5130 position:absolute; 5131 top:8px; 5132 left:8px; 5133 background:#dc2626; 5134 color:#fff; 5135 font-size:.6rem; 5136 padding:4px 6px 3px; 5137 border-radius:6px; 5138 font-weight:700; 5139 letter-spacing:.4px; 5140} 5141 5142.pa-card .body { 5143 padding:.65rem .7rem .8rem; 5144 display:flex; 5145 flex-direction:column; 5146 gap:.45rem; 5147 min-height:90px; 5148} 5149 5150.pa-card .title { 5151 font-size:.75rem; 5152 line-height:1.25; 5153 margin:0; 5154 font-weight:600;
5155 color:#1e293b; 5156 height:2.4em; 5157 overflow:hidden; 5158} 5159 5160.pa-card .prices { 5161 display:flex; 5162 flex-wrap:wrap; 5163 align-items:baseline; 5164 gap:.4rem; 5165} 5166.pa-card .prices .final { 5167 font-size:.83rem; 5168 font-weight:700; 5169 color:#0d6efd; 5170} 5171.pa-card .prices .ori { 5172 font-size:.63rem; 5173 text-decoration:line-through; 5174 color:#94a3b8; 5175 font-weight:500; 5176} 5177 5178.pa-card .cat { 5179 margin-top:auto; 5180 font-size:.55rem; 5181 letter-spacing:.6px; 5182 text-transform:uppercase; 5183 background:#f1f5f9; 5184 border:1px solid #e2e8f0; 5185 padding:.28rem .45rem .25rem; 5186 border-radius:20px; 5187 font-weight:600; 5188 color:#475569; 5189 align-self:flex-start; 5190} 5191.pa-card .meta-line { 5192 margin-top:4px; 5193 font-size:.65rem; 5194 color:#475569; 5195} 5196.pa-card .meta-line .sold { 5197 display:inline-block; 5198 background:#f1f5f9; 5199 padding:2px 6px 3px; 5200 border-radius:4px; 5201 font-weight:600; 5202 letter-spacing:.3px; 5203 color:#334155; 5204} 5205 5206/* Pagination */ 5207.pa-pagination { 5208 margin-top:2rem; 5209 display:flex; 5210 flex-wrap:wrap; 5211 gap:.4rem; 5212 justify-content:center; 5213 font-size:.7rem; 5214 align-items:center; 5215} 5216.pa-pagination a, 5217.pa-pagination span { 5218 min-width:34px; 5219 height:34px; 5220 display:flex; 5221 align-items:center; 5222 justify-content:center; 5223 border-radius:8px; 5224 text-decoration:none; 5225 font-weight:600; 5226 padding:0 .6rem; 5227 box-sizing:border-box; 5228 border:1px solid #e2e8f0; 5229 background:#fff; 5230 color:#334155; 5231 transition:.25s; 5232} 5233.pa-pagination a:hover { 5234 background:#f1f5f9; 5235 color:#0f172a; 5236} 5237.pa-pagination .current { 5238 background:#0d6efd; 5239 color:#fff; 5240 border-color:#0d6efd; 5241} 5242.pa-pagination .gap { 5243 border:none; 5244 background:transparent; 5245 font-weight:400; 5246 color:#94a3b8; 5247} 5248 5249/* =============== Mobile Filter Bar (Tokopedia-like) =============== */ 5250.pa-mobile-filterbar{display:none} 5251@media (max-width: 640px){ 5252 /* sembunyikan tombol lama */ 5253 .pa-btn-filter{display:none} 5254 5255 .pa-mobile-filterbar{ 5256 display:block; 5257 position:sticky; 5258 top:0; /* sesuaikan jika ada header fixed di atas */ 5259 z-index:5; 5260 background:#fff; 5261 border-bottom:1px solid #e2e8f0; 5262 padding:.4rem .5rem; 5263 margin:-.4rem -1rem .6rem; /* full-bleed di container mobile */ 5264 } 5265 .pa-mobile-filterbar .mf-scroll{ 5266 display:flex;align-items:center;gap:.5rem; 5267 overflow-x:auto; 5268 -webkit-overflow-scrolling:touch; 5269 scrollbar-width:none; 5270 } 5271 .pa-mobile-filterbar .mf-scroll::-webkit-scrollbar{display:none} 5272 5273 .mf-chip{ 5274 flex:0 0 auto; 5275 background:#fff; 5276 color:#334155; 5277 text-decoration:none; 5278 border:1px solid #e2e8f0; 5279 border-radius:999px; 5280 padding:.44rem .7rem; 5281 font-size:.78rem; 5282 font-weight:600; 5283 letter-spacing:.2px; 5284 white-space:nowrap; 5285 } 5286 .mf-chip:active{transform:scale(.98)} 5287 .mf-chip.active{ 5288 background:#eaf2ff; 5289 border-color:#0d6efd; 5290 color:#0d6efd; 5291 } 5292 5293 .mf-filter-btn{ 5294 position:relative; 5295 flex:0 0 auto; 5296 width:38px;height:38px; 5297 border-radius:50%; 5298 border:1.5px solid #0d6efd; 5299 background:#fff; 5300 display:inline-flex;align-items:center;justify-content:center; 5301 color:#0d6efd; 5302 cursor:pointer; 5303 } 5304 .mf-filter-btn:active{transform:scale(.96)} 5305 .mf-filter-btn svg{width:18px;height:18px;display:block} 5306 .mf-filter-btn .mf-badge{ 5307 position:absolute;top:0;right:-4px; 5308 min-width:18px;height:18px;border-radius:9px; 5309 background:#ef4444;color:#fff; 5310 font-size:.65rem;font-weight:700; 5311 display:flex;align-items:center;justify-content:center; 5312 padding:0 4px;line-height:1; 5313 box-shadow:0 2px 6px -2px rgba(239,68,68,.6); 5314 } 5315} 5316 5317/* Responsive */ 5318@media (max-width: 640px) { 5319 .product-archive { padding: 0 1rem 3rem; } 5320 .pa-grid { grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); } 5321 .pa-search input[type=text]{ min-width:140px; } 5322 .pa-toolbar { flex-direction:column; align-items:stretch; margin-bottom: 0; display: none;} 5323 .pa-right { display: none; } 5324 .pa-sort { width: 100%; justify-content:center; } 5325 .pa-mobile-filterbar { display: flex !important; } 5326} 5327 5328.pa-mobile-filterbar { 5329 display: none; 5330 align-items: center; 5331 gap: 8px; 5332 padding: 8px 0 8px 12px; 5333 background: #fff; 5334 width: 100vw; 5335} 5336.pa-mobile-filterbar .mf-scroll { 5337 display: flex; 5338 gap: 8px; 5339 overflow-x: auto; 5340 -webkit-overflow-scrolling: touch; 5341 scrollbar-width: none; /* Firefox */ 5342 flex: 1 1 auto; /* takes remaining width */ 5343} 5344.pa-mobile-filterbar .mf-scroll::-webkit-scrollbar { display: none; } /* WebKit */ 5345.pa-mobile-filterbar .mf-chip { 5346 flex: 0 0 auto; /* prevent shrinking, keep width based on content */ 5347 white-space: nowrap; 5348} 5349.pa-mobile-filterbar .mf-filter-btn { 5350 flex: 0 0 auto; /* do not shrink */ 5351 position: relative; /* keep in place, not inside scroller */ 5352 display: inline-flex; 5353 align-items: center; 5354 gap: 6px; 5355} 5356 5357/* Optional: ensure on small screens the button stays visible with min width */ 5358@media (max-width: 768px) { 5359 .pa-mobile-filterbar .mf-filter-btn { 5360 padding: 6px 10px; 5361 } 5362} 5363 5364/* Product Specification Chips */ 5365.pa-card .spec-chips { 5366 font-size: .6rem; 5367 color: #64748b; 5368 font-weight: 500; 5369 letter-spacing: .2px; 5370 margin-top: 2px; 5371 margin-bottom: 4px;
5372 line-height: 1.2; 5373 opacity: 0.9; 5374 max-width: 100%; 5375 overflow: hidden; 5376 text-overflow: ellipsis; 5377 white-space: nowrap; 5378} 5379 5380.pa-card .spec-chips .spec-divider { 5381 color: #94a3b8; 5382 font-weight: 400; 5383 margin: 0 1px; 5384} 5385</style> 5386 <style data-inlined="site-notice.css"> 5387/** 5388 * Site Notice Banner - Reusable Notification System 5389 * Dismissible top bar for announcements, holidays, maintenance, etc. 5390 */ 5391 5392.holiday-notice { 5393 position: fixed; 5394 top: 0; 5395 left: 0; 5396 right: 0; 5397 z-index: 9999; 5398 background: linear-gradient(135deg, #d32f2f 0%, #f57c00 100%); 5399 border-bottom: 2px solid rgba(255, 255, 255, 0.2); 5400 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 5401 animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1); 5402 transform-origin: top; 5403} 5404 5405.holiday-notice.hidden { 5406 animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 5407} 5408 5409@keyframes slideDown { 5410 from { 5411 transform: translateY(-100%); 5412 opacity: 0; 5413 } 5414 to { 5415 transform: translateY(0); 5416 opacity: 1; 5417 } 5418} 5419 5420@keyframes slideUp { 5421 from { 5422 transform: translateY(0); 5423 opacity: 1; 5424 } 5425 to { 5426 transform: translateY(-100%); 5427 opacity: 0; 5428 } 5429} 5430 5431.holiday-notice-container { 5432 max-width: 1400px; 5433 margin: 0 auto; 5434 padding: 12px 20px; 5435 display: flex;
5436 align-items: center; 5437 justify-content: space-between; 5438 gap: 16px; 5439} 5440 5441.holiday-notice-content { 5442 display: flex; 5443 align-items: center; 5444 gap: 12px; 5445 flex: 1; 5446} 5447 5448.holiday-notice-icon { 5449 font-size: 20px; 5450 line-height: 1; 5451 flex-shrink: 0; 5452} 5453 5454.holiday-notice-text { 5455 color: #ffffff; 5456 font-size: 14px; 5457 font-weight: 500; 5458 line-height: 1.5; 5459 margin: 0; 5460 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 5461} 5462 5463.holiday-notice-emoji { 5464 display: inline-block; 5465 animation: bounce 2s ease-in-out infinite; 5466} 5467 5468@keyframes bounce { 5469 0%, 100% { 5470 transform: translateY(0); 5471 } 5472 50% { 5473 transform: translateY(-4px); 5474 } 5475} 5476 5477.holiday-notice-close { 5478 background: rgba(255, 255, 255, 0.2); 5479 border: 1px solid rgba(255, 255, 255, 0.3); 5480 border-radius: 6px; 5481 padding: 6px; 5482 cursor: pointer; 5483 color: #ffffff; 5484 transition: all 0.2s ease; 5485 flex-shrink: 0; 5486 display: flex; 5487 align-items: center; 5488 justify-content: center; 5489 width: 32px; 5490 height: 32px; 5491} 5492 5493.holiday-notice-close:hover { 5494 background: rgba(255, 255, 255, 0.3); 5495 transform: scale(1.05); 5496} 5497 5498.holiday-notice-close:active { 5499 transform: scale(0.95); 5500} 5501 5502.holiday-notice-close:focus { 5503 outline: 2px solid rgba(255, 255, 255, 0.5); 5504 outline-offset: 2px; 5505} 5506 5507/* Body padding adjustment when banner is visible */ 5508body.has-holiday-notice { 5509 padding-top: 52px; 5510} 5511 5512/* Tablet Styles */ 5513@media (max-width: 1024px) { 5514 .holiday-notice-container { 5515 padding: 10px 16px; 5516 } 5517 5518 .holiday-notice-text { 5519 font-size: 13px; 5520 } 5521 5522 .holiday-notice-icon { 5523 font-size: 18px; 5524 } 5525 5526 body.has-holiday-notice { 5527 padding-top: 48px; 5528 } 5529} 5530 5531/* Mobile Styles */ 5532@media (max-width: 640px) { 5533 .holiday-notice-container { 5534 padding: 10px 12px; 5535 gap: 8px; 5536 } 5537 5538 .holiday-notice-content { 5539 gap: 8px; 5540 } 5541 5542 .holiday-notice-text { 5543 font-size: 12px; 5544 line-height: 1.4; 5545 } 5546 5547 .holiday-notice-icon { 5548 font-size: 16px; 5549 } 5550 5551 .holiday-notice-close { 5552 width: 28px; 5553 height: 28px; 5554 } 5555 5556 .holiday-notice-close svg { 5557 width: 16px; 5558 height: 16px; 5559 } 5560 5561 body.has-holiday-notice { 5562 padding-top: 44px; 5563 } 5564} 5565 5566/* Ensure header doesn't get covered - removed z-index as it conflicts with dropdown stacking */ 5567/* .header, 5568.navbar, 5569header { 5570 position: relative; 5571 z-index: 100; 5572} */ 5573 5574</style> 5575 <style data-inlined="benchmark.css"> 5576/** 5577 * Benchmark Section Styles 5578 * For product detail page benchmark display 5579 */ 5580 5581.pd-benchmark-section { 5582 background: #fff; 5583 border-radius: 12px; 5584 padding: 24px; 5585 margin: 32px 0; 5586 box-shadow: 0 2px 8px rgba(0,0,0,0.06); 5587 border: 1px solid #e5e7eb; 5588} 5589 5590.benchmark-header { 5591 margin-bottom: 20px; 5592} 5593 5594.benchmark-title { 5595 font-size: 22px; 5596 font-weight: 700; 5597 color: #1a1a1a; 5598 margin: 0 0 12px 0; 5599 display: flex;
5600 align-items: center; 5601 gap: 8px; 5602} 5603 5604.benchmark-hint { 5605 font-size: 14px; 5606 color: #6b7280; 5607 margin: 0; 5608} 5609 5610/* Preview Badges */ 5611.benchmark-preview-badges { 5612 display: flex; 5613 flex-wrap: wrap; 5614 gap: 10px; 5615 margin-top: 12px; 5616} 5617 5618.benchmark-badge { 5619 display: inline-flex; 5620 align-items: center; 5621 gap: 6px; 5622 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5623 color: #fff; 5624 padding: 8px 16px; 5625 border-radius: 20px; 5626 font-size: 13px; 5627 font-weight: 500; 5628 box-shadow: 0 2px 6px rgba(102, 126, 234, 0.25); 5629 transition: all 0.2s ease; 5630} 5631 5632.benchmark-badge:hover { 5633 transform: translateY(-2px); 5634 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35); 5635} 5636 5637.benchmark-badge strong { 5638 font-weight: 600; 5639 opacity: 0.9; 5640} 5641 5642/* Load Button */ 5643.btn-load-benchmark { 5644 display: flex; 5645 align-items: center; 5646 justify-content: center; 5647 gap: 10px; 5648 width: 100%; 5649 max-width: 420px; 5650 padding: 14px 24px; 5651 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5652 color: #fff; 5653 border: none; 5654 border-radius: 8px; 5655 font-size: 15px; 5656 font-weight: 600; 5657 cursor: pointer; 5658 transition: all 0.3s ease; 5659 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 5660 margin: 0 auto; 5661} 5662 5663.btn-load-benchmark:hover:not(:disabled) { 5664 transform: translateY(-2px); 5665 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); 5666} 5667 5668.btn-load-benchmark:disabled { 5669 opacity: 0.6; 5670 cursor: not-allowed; 5671} 5672 5673.btn-load-benchmark .btn-icon { 5674 font-size: 20px; 5675} 5676 5677.btn-load-benchmark.loading .btn-icon { 5678 animation: spin 1s linear infinite; 5679} 5680 5681@keyframes spin { 5682 from { transform: rotate(0deg); } 5683 to { transform: rotate(360deg); } 5684} 5685 5686/* Benchmark Content */ 5687.benchmark-content { 5688 margin-top: 24px; 5689 padding-top: 24px; 5690 border-top: 2px solid #f3f4f6; 5691} 5692 5693.benchmark-loading { 5694 text-align: center; 5695 padding: 40px 20px; 5696 color: #6b7280; 5697} 5698 5699.benchmark-loading-spinner { 5700 width: 48px; 5701 height: 48px; 5702 margin: 0 auto 16px; 5703 border: 4px solid #f3f4f6; 5704 border-top-color: #667eea; 5705 border-radius: 50%; 5706 animation: spin 1s linear infinite; 5707} 5708 5709.benchmark-error { 5710 text-align: center; 5711 padding: 32px 20px; 5712 color: #ef4444; 5713 background: #fee2e2; 5714 border-radius: 8px; 5715} 5716 5717/* Benchmark Data Display */ 5718.benchmark-data-grid { 5719 display: grid; 5720 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 5721 gap: 16px; 5722 margin-bottom: 24px; 5723} 5724 5725.benchmark-card { 5726 background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); 5727 border: 1px solid #e5e7eb; 5728 border-radius: 10px; 5729 padding: 18px; 5730 text-align: center; 5731 transition: all 0.2s ease; 5732} 5733 5734.benchmark-card:hover { 5735 transform: translateY(-3px); 5736 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.15); 5737} 5738 5739.benchmark-card-label { 5740 font-size: 13px; 5741 color: #6b7280; 5742 font-weight: 500; 5743 margin-bottom: 8px; 5744 text-transform: uppercase; 5745 letter-spacing: 0.5px; 5746} 5747 5748.benchmark-card-value { 5749 font-size: 24px; 5750 font-weight: 700; 5751 color: #1a1a1a; 5752 line-height: 1.2; 5753} 5754 5755.benchmark-card-value.large { 5756 font-size: 28px; 5757} 5758 5759.benchmark-card-subtitle { 5760 font-size: 12px; 5761 color: #9ca3af; 5762 margin-top: 4px; 5763} 5764 5765/* Spec Details */ 5766.benchmark-specs { 5767 background: #f9fafb; 5768 border-radius: 10px; 5769 padding: 20px; 5770 margin-top: 20px; 5771} 5772 5773.benchmark-specs-title { 5774 font-size: 16px; 5775 font-weight: 600; 5776 color: #1a1a1a; 5777 margin: 0 0 16px 0; 5778} 5779 5780.benchmark-spec-row { 5781 display: flex; 5782 justify-content: space-between; 5783 padding: 10px 0; 5784 border-bottom: 1px solid #e5e7eb; 5785} 5786 5787.benchmark-spec-row:last-child { 5788 border-bottom: none; 5789} 5790 5791.benchmark-spec-label { 5792 font-size: 14px; 5793 color: #6b7280; 5794 font-weight: 500; 5795} 5796 5797.benchmark-spec-value { 5798 font-size: 14px; 5799 color: #1a1a1a; 5800 font-weight: 600; 5801 text-align: right; 5802} 5803 5804/* Score Badges */ 5805.benchmark-score-badge { 5806 display: inline-block; 5807 padding: 6px 14px; 5808 border-radius: 20px; 5809 font-size: 13px; 5810 font-weight: 600; 5811 margin-top: 8px; 5812} 5813 5814.benchmark-score-badge.excellent { 5815 background: #10b981; 5816 color: #fff; 5817} 5818 5819.benchmark-score-badge.good { 5820 background: #3b82f6; 5821 color: #fff; 5822} 5823 5824.benchmark-score-badge.average { 5825 background: #f59e0b; 5826 color: #fff; 5827} 5828 5829.benchmark-score-badge.poor { 5830 background: #ef4444; 5831 color: #fff; 5832} 5833 5834/* Attribution */ 5835.benchmark-attribution { 5836 text-align: center; 5837 margin-top: 24px; 5838 padding-top: 20px; 5839 border-top: 1px solid #e5e7eb; 5840 font-size: 13px; 5841 color: #9ca3af; 5842} 5843 5844.benchmark-attribution a { 5845 color: #667eea; 5846 text-decoration: none; 5847 font-weight: 500; 5848} 5849 5850.benchmark-attribution a:hover { 5851 text-decoration: underline; 5852} 5853 5854/* Responsive */ 5855@media (max-width: 768px) { 5856 .pd-benchmark-section { 5857 padding: 16px; 5858 margin: 20px 0; 5859 } 5860 5861 .benchmark-title { 5862 font-size: 18px; 5863 } 5864 5865 .benchmark-preview-badges { 5866 flex-direction: column; 5867 gap: 8px; 5868 } 5869 5870 .benchmark-badge { 5871 justify-content: center; 5872 } 5873 5874 .btn-load-benchmark { 5875 font-size: 14px; 5876 padding: 12px 20px; 5877 } 5878 5879 .benchmark-data-grid { 5880 grid-template-columns: 1fr 1fr; 5881 gap: 12px; 5882 } 5883 5884 .benchmark-card { 5885 padding: 14px; 5886 } 5887 5888 .benchmark-card-value { 5889 font-size: 20px; 5890 } 5891 5892 .benchmark-spec-row { 5893 flex-direction: column; 5894 gap: 4px; 5895 } 5896 5897 .benchmark-spec-value { 5898 text-align: left; 5899 } 5900} 5901 5902@media (max-width: 480px) { 5903 .benchmark-data-grid { 5904 grid-template-columns: 1fr; 5905 } 5906} 5907 5908</style> 5909 <style data-inlined="product-filters.css"> 5910/* ========================================================= 5911 Product Filters (Desktop + Mobile Drawer) 5912 Light Only + FIX: 5913 - Desktop: f-actions selalu tampil (static bottom) 5914 - Mobile: konten filter dapat discroll 5915 ========================================================= */ 5916 5917/* ========== Layout wrapper (tidak mengubah grid produk) ========== */ 5918.product-archive.with-filters .pa-layout { 5919 display:flex; 5920 gap:24px; 5921 align-items:flex-start; 5922} 5923.pa-main { flex:1; min-width:0; } 5924 5925/* ========================================================= 5926 DESKTOP PANEL 5927 ========================================================= */ 5928.pa-filters { 5929 width:300px; 5930 background:linear-gradient(145deg,#ffffff 0%,#f8fafc 55%,#f1f5f9 100%); 5931 border:1px solid #e2e8f0; 5932 padding:18px 18px 12px; /* bottom dikurangi karena f-actions punya padding sendiri */ 5933 border-radius:18px; 5934 position:sticky; 5935 top:80px; 5936 height:calc(100vh - 100px); /* tinggi stabil untuk inner scroll */ 5937 font-size:14px; 5938 box-shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(15,23,42,.08); 5939 backdrop-filter:blur(3px); 5940 -webkit-backdrop-filter:blur(3px);
5941 transition:box-shadow .3s, background .4s; 5942 display:flex; 5943 flex-direction:column; 5944 overflow:hidden; /* cegah double scroll, inner yang scroll */ 5945} 5946.pa-filters::before { 5947 content:""; 5948 position:absolute; 5949 inset:0 auto 0 0; 5950 width:7px; 5951 border-radius:18px 0 0 18px; 5952 background:linear-gradient(180deg,#f59e0b 0%,#ffb53a 35%,#1d6ce0 100%); 5953} 5954.pa-filters.active { box-shadow:0 8px 32px -4px rgba(15,23,42,.35); } 5955 5956/* Scroll area (desktop/mode shared) */ 5957.filters-scroll { 5958 flex:1; 5959 min-height:0; /* wajib agar overflow terbaca dalam flex container */ 5960 overflow:auto; 5961 padding-right:6px; 5962 margin-right:-6px; /* kompensasi scrollbar */ 5963 scrollbar-width:thin; 5964 overscroll-behavior:contain; 5965} 5966.filters-scroll::-webkit-scrollbar { width:8px; } 5967.filters-scroll::-webkit-scrollbar-thumb { 5968 background:#cbd5e1; 5969 border-radius:20px; 5970 border:2px solid #f1f5f9; 5971} 5972.filters-scroll::-webkit-scrollbar-thumb:hover { background:#94a3b8; } 5973 5974/* Heading */ 5975.pa-filters h2 { 5976 margin:0 0 14px; 5977 font-size:20px; 5978 font-weight:700; 5979 letter-spacing:.4px; 5980 line-height:1.15; 5981 background:linear-gradient(90deg,#0f172a,#1d6ce0 60%,#0f172a); 5982 -webkit-background-clip:text; 5983 color:transparent; 5984 background-size:200% 100%; 5985 animation:titleSheen 8s linear infinite; 5986} 5987@keyframes titleSheen { 5988 0%{background-position:0 0} 5989 50%{background-position:100% 0} 5990 100%{background-position:0 0} 5991} 5992 5993/* Form groups */ 5994.pa-filters .f-group { margin-bottom:18px; position:relative; } 5995.pa-filters label { 5996 display:block; 5997 font-weight:600; 5998 margin:0 0 6px; 5999 font-size:13px; 6000 color:#0f172a; 6001 letter-spacing:.3px; 6002} 6003 6004/* Inputs / selects */ 6005.pa-filters input[type=text], 6006.pa-filters input[type=number], 6007.pa-filters select { 6008 width:100%; 6009 padding:7px 10px; 6010 border:1px solid #cbd5e1; 6011 border-radius:8px; 6012 font-size:13px; 6013 min-height:34px; 6014 background:#ffffff; 6015 box-sizing:border-box; 6016 color:#0f172a; 6017 transition:border-color .25s, box-shadow .25s, background .25s; 6018 -webkit-appearance:none; 6019 -moz-appearance:none; 6020 appearance:none; 6021} 6022.pa-filters select { 6023 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"); 6024 background-repeat:no-repeat; 6025 background-position:right 10px center; 6026 background-size:12px; 6027 padding-right:32px; 6028} 6029.pa-filters input[type=text]:hover, 6030.pa-filters input[type=number]:hover, 6031.pa-filters select:hover { border-color:#94b5ec; } 6032.pa-filters input[type=text]:focus, 6033.pa-filters input[type=number]:focus, 6034.pa-filters select:focus { 6035 outline:none; 6036 border-color:#1d6ce0; 6037 box-shadow:0 0 0 3px rgba(29,108,224,.25); 6038 background:#f8fbff; 6039} 6040.pa-filters .range { display:flex; gap:10px; } 6041.pa-filters .range input { flex:1; } 6042.pa-filters .hint { font-size:11px; color:#64748b; margin-top:6px; } 6043 6044/* Spec blocks */ 6045.f-specs { margin-top:4px; } 6046.spec-attr { 6047 margin:0 0 14px; 6048 padding:12px 12px 10px; 6049 border:1px solid #e2e8f0; 6050 border-radius:14px; 6051 background:linear-gradient(130deg,#ffffff 0%,#f7faff 85%); 6052 position:relative; 6053 transition:border-color .3s, box-shadow .3s, background .4s; 6054} 6055.spec-attr.collapsed { padding-bottom:8px; } 6056.spec-attr.has-selected { 6057 border-color:#1d6ce0; 6058 box-shadow:0 0 0 2px rgba(29,108,224,.15),0 2px 8px -2px rgba(29,108,224,.35); 6059 background:linear-gradient(135deg,#ffffff 0%,#e9f2ff 100%); 6060} 6061.spec-attr-title { 6062 font-weight:600; 6063 margin:0 0 8px; 6064 font-size:13px; 6065 display:flex; 6066 justify-content:space-between; 6067 align-items:center; 6068 cursor:pointer; 6069 color:#0f172a; 6070 user-select:none; 6071} 6072.spec-attr-title .chevron { 6073 font-size:10px; 6074 transition:transform .25s, opacity .25s; 6075 opacity:.65; 6076} 6077.spec-attr.collapsed .spec-attr-title .chevron { 6078 transform:rotate(-90deg); 6079 opacity:.5; 6080} 6081 6082/* Spec options */ 6083.spec-attr-options { display:flex; flex-wrap:wrap; gap:6px; } 6084.spec-attr-options label { 6085 display:inline-flex; 6086 align-items:center; 6087 gap:4px; 6088 font-weight:500; 6089 border:1px solid #d4dde7; 6090 padding:6px 9px; 6091 border-radius:30px; 6092 cursor:pointer; 6093 background:#f3f6fa; 6094 font-size:12px; 6095 line-height:1.1; 6096 transition:background .25s, border-color .25s, color .25s, box-shadow .25s; 6097 position:relative; 6098} 6099.spec-attr-options label:hover { background:#e8f1ff; border-color:#b1cff3; } 6100.spec-attr-options input { 6101 margin:0; 6102 position:absolute; 6103 inset:0; 6104 opacity:0;
6105 cursor:pointer; 6106} 6107.spec-attr-options label:has(input:checked) { 6108 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6109 border-color:#1d6ce0; 6110 color:#ffffff; 6111 box-shadow:0 2px 6px -1px rgba(29,108,224,.55); 6112} 6113.spec-attr-options label:has(input:checked)::after { 6114 content:""; 6115 position:absolute; 6116 right:6px; 6117 top:6px; 6118 width:6px; 6119 height:6px; 6120 background:#fff; 6121 border-radius:50%; 6122 box-shadow:0 0 0 4px rgba(255,255,255,.25),0 0 0 7px rgba(29,108,224,.5); 6123 animation:pulseDot 1.9s ease-out infinite; 6124} 6125@keyframes pulseDot { 6126 0%{transform:scale(.8);opacity:.9} 6127 60%{transform:scale(1.4);opacity:.15} 6128 100%{transform:scale(.8);opacity:.9} 6129} 6130 6131/* Badges */ 6132.pa-filter-badges { margin:0 0 18px; display:flex; flex-wrap:wrap; gap:6px; } 6133.pa-filter-badge { 6134 display:inline-flex; 6135 gap:6px; 6136 align-items:center; 6137 background:linear-gradient(135deg,#eef4ff 0%,#e0edff 100%); 6138 border:1px solid #b4cff3; 6139 padding:5px 10px 5px 12px; 6140 border-radius:22px; 6141 margin:2px 4px 0 0; 6142 font-size:11px; 6143 font-weight:500; 6144 line-height:1; 6145 color:#0f172a; 6146 position:relative; 6147 overflow:hidden; 6148} 6149.pa-filter-badge::before { 6150 content:""; 6151 position:absolute; 6152 inset:0; 6153 background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.35),rgba(255,255,255,0)); 6154 transform:translateX(-100%); 6155 animation:badgeSheen 6s linear infinite; 6156} 6157@keyframes badgeSheen { 6158 0%{transform:translateX(-100%)} 6159 55%{transform:translateX(120%)} 6160 100%{transform:translateX(120%)} 6161} 6162.pa-filter-badge a.clear { 6163 text-decoration:none; 6164 color:#c02828; 6165 font-weight:700; 6166 padding:0 3px; 6167 display:inline-flex; 6168 align-items:center; 6169 justify-content:center; 6170 border-radius:50%; 6171 line-height:1; 6172 transition:background .25s, color .25s; 6173} 6174.pa-filter-badge a.clear:hover { background:#ffeded; color:#a40000; } 6175.pa-btn-filter { width: 100%;} 6176 6177/* ========================================================= 6178 ACTIONS (DESKTOP) - STATIC BOTTOM 6179 ========================================================= */ 6180.f-actions { 6181 margin-top:10px; 6182 display:flex; 6183 gap:12px; 6184 flex-wrap:wrap; 6185 padding:16px 0 4px; 6186 border-top:1px solid #e2e8f0; 6187 background:linear-gradient(180deg,rgba(255,255,255,0) 0%,#f1f5f9 70%); 6188 flex-shrink:0; 6189} 6190.f-actions button { 6191 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6192 color:#fff; 6193 border:none; 6194 padding:10px 20px; 6195 border-radius:12px; 6196 cursor:pointer; 6197 font-size:14px; 6198 font-weight:600; 6199 letter-spacing:.3px; 6200 display:inline-flex; 6201 align-items:center; 6202 gap:6px; 6203 box-shadow:0 4px 14px -2px rgba(29,108,224,.45); 6204 transition:transform .25s, box-shadow .3s; 6205} 6206.f-actions button:hover { transform:translateY(-3px); box-shadow:0 8px 26px -4px rgba(29,108,224,.6); } 6207.f-actions button:active { transform:translateY(-1px) scale(.98); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6208.f-actions .reset { 6209 font-size:12px; 6210 text-decoration:none; 6211 color:#d92d2d; 6212 align-self:center; 6213 font-weight:600; 6214 padding:4px 6px; 6215 border-radius:8px; 6216 transition:background .25s, color .25s; 6217} 6218.f-actions .reset:hover { background:#ffe6e6; color:#b80000; text-decoration:underline; } 6219.pa-filters .close-drawer { display: none; } 6220 6221/* Form wrapper: flex container agar inner .filters-scroll bisa scroll di desktop */ 6222.filter-form { 6223 flex: 1; 6224 min-height: 0; 6225 display: flex; 6226 flex-direction: column; 6227 overflow: hidden; 6228} 6229 6230/* ========================================================= 6231 FILTER TOGGLE (MOBILE TRIGGER) 6232 ========================================================= */ 6233.filter-toggle { 6234 display:none; 6235 margin:0 0 14px; 6236 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6237 color:#fff; 6238 border:none; 6239 padding:11px 18px; 6240 font-size:14px; 6241 font-weight:600; 6242 border-radius:14px; 6243 cursor:pointer; 6244 letter-spacing:.4px; 6245 box-shadow:0 4px 14px -2px rgba(29,108,224,.55); 6246 position:relative; 6247 z-index:100; 6248 transition:box-shadow .3s, transform .25s; 6249} 6250.filter-toggle:hover { box-shadow:0 8px 26px -4px rgba(29,108,224,.6); transform:translateY(-2px); } 6251.filter-toggle:active { transform:translateY(0); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6252.filter-toggle .count-badge { 6253 display:inline-block; 6254 background:linear-gradient(135deg,#f59e0b 0%,#ffb43d 55%,#f59e0b 100%); 6255 color:#1d2b44; 6256 font-size:11px; 6257 font-weight:700; 6258 padding:3px 7px 4px;
6259 border-radius:30px; 6260 margin-left:8px; 6261 box-shadow:0 0 0 2px #ffffff,0 0 0 4px #1d6ce0; 6262} 6263 6264/* Overlay (mobile) */ 6265.pa-filters-overlay { display:none; } 6266 6267/* ========================================================= 6268 MOBILE DRAWER 6269 ========================================================= */ 6270@media (max-width:768px){ 6271 .product-archive.with-filters .pa-layout { flex-direction:column; gap:12px; } 6272 .filter-toggle { display:inline-flex; align-items:center; gap:6px; } 6273 6274 .pa-filters-overlay { 6275 position:fixed; 6276 inset:0; 6277 background:rgba(0,20,45,.55); 6278 z-index:998; 6279 opacity:0; 6280 transition:opacity .35s; 6281 display:block; 6282 pointer-events:none; 6283 backdrop-filter:blur(2px); 6284 } 6285 .pa-filters-overlay.active { opacity:1; pointer-events:auto; } 6286 6287 .pa-filters { 6288 position:fixed; 6289 top:auto; 6290 left:0; right:0; bottom:0; 6291 max-height:92vh; 6292 height:auto; 6293 width:100%; 6294 border-radius:30px 30px 0 0; 6295 border:1px solid #dbe4ef; 6296 padding:26px 22px 0; 6297 z-index:10000; /* di atas bottom-nav (9998) */ 6298 transform:translateY(102%); 6299 transition:transform .55s cubic-bezier(.68,-0.35,.25,1); 6300 box-shadow:0 -4px 40px -6px rgba(15,23,42,.45); 6301 overflow:hidden; /* outer hidden, inner scroll */ 6302 } 6303 .pa-filters.active { transform:translateY(0); } 6304 6305 /* Drag handle indicator */ 6306 .pa-filters::after { 6307 content:""; 6308 display:block; 6309 position:absolute; 6310 top:10px; 6311 left:50%; 6312 transform:translateX(-50%); 6313 width:36px; 6314 height:4px; 6315 background:#cbd5e1; 6316 border-radius:4px; 6317 } 6318 body.filter-open { overflow:hidden; touch-action:none; } 6319 6320 /* Restore mobile-friendly input sizes (prevent iOS zoom on focus) */ 6321 .pa-filters input[type=text], 6322 .pa-filters input[type=number], 6323 .pa-filters select { 6324 font-size:16px; 6325 min-height:44px; 6326 padding:10px 12px; 6327 border-radius:10px; 6328 } 6329 .pa-filters select { padding-right:32px; } 6330 6331 .filters-scroll { 6332 flex:1; 6333 overflow:auto; 6334 padding:0 0 120px; /* ruang di atas bar actions */ 6335 margin:0; 6336 -webkit-overflow-scrolling:touch; 6337 } 6338 6339 .pa-filters h2 { font-size:19px; margin-bottom:10px; } 6340 6341 .pa-filters .close-drawer { 6342 display: block; 6343 position:absolute; 6344 right:14px; top:12px; 6345 background:linear-gradient(135deg,#dc2626 0%,#b91c1c 100%); 6346 color:#fff; 6347 padding:7px 14px; 6348 border:none; 6349 border-radius:10px; 6350 font-size:12px; 6351 font-weight:600; 6352 cursor:pointer; 6353 box-shadow:0 3px 10px -2px rgba(220,38,38,.55); 6354 transition:transform .25s, box-shadow .3s; 6355 z-index: 99; 6356 } 6357 .pa-filters .close-drawer:hover { transform:translateY(-2px); box-shadow:0 6px 18px -4px rgba(220,38,38,.6); } 6358 6359 .spec-attr { border-radius:18px; } 6360 .spec-attr-options label { font-size:11px; padding:5px 9px; } 6361 6362 /* Actions fixed di viewport bawah */ 6363 .f-actions { 6364 position:fixed; 6365 left:0; right:0; bottom:0; 6366 margin:0; 6367 border-top:1px solid #d6dee8; 6368 padding:14px 16px calc(16px + env(safe-area-inset-bottom)); 6369 background:#ffffffea; 6370 backdrop-filter:blur(6px); 6371 box-shadow:0 -4px 22px -6px rgba(15,23,42,.25); 6372 justify-content:space-between; 6373 z-index:10001; /* di atas pa-filters (10000) dan bottom-nav (9998) */ 6374 } 6375 6376 /* Sembunyikan bottom-nav saat filter drawer terbuka */ 6377 body.filter-open .bottom-nav { 6378 display: none !important; 6379 } 6380 .f-actions button { 6381 flex:1 1 auto; 6382 justify-content:center; 6383 padding:14px 14px; 6384 font-size:15px; 6385 border-radius:14px; 6386 } 6387 .f-actions .reset { 6388 order:-1; 6389 font-size:12px; 6390 padding:2px 8px; 6391 } 6392} 6393 6394/* Scrollbar (outer panel desktop only kept for compatibility) */ 6395.pa-filters::-webkit-scrollbar { width:0; } /* disembunyikan karena inner yang scroll */ 6396 6397/* Utilities */ 6398.hidden { display:none !important; } 6399.gradient-text { 6400 background:linear-gradient(90deg,#1d6ce0,#155ab8,#1d6ce0); 6401 -webkit-background-clip:text; 6402 color:transparent; 6403 animation:titleSheen 10s linear infinite; 6404} 6405.glow-focus:focus {
6406 outline:none !important; 6407 box-shadow:0 0 0 3px rgba(29,108,224,.4) !important; 6408 border-color:#1d6ce0 !important; 6409} 6410 6411/* Simple fade (opsional) */ 6412@keyframes fadeIn { 6413 from { opacity:0; transform:translateY(8px); } 6414 to { opacity:1; transform:translateY(0); } 6415} 6416</style> 6417 <style data-inlined="mega-menu-tiles.css"> 6418/* Tile Mega Menu â Modern E-commerce Enhanced (2025) */ 6419 6420/* Design Tokens */ 6421:root { 6422 --mm2-bg: #ffffff; 6423 --mm2-surface: #ffffff; 6424 --mm2-surface-elevated: rgba(255,255,255,0.98); 6425 --mm2-muted: #f8fafc; 6426 --mm2-border: #e2e8f0; 6427 --mm2-border-strong: #cbd5e1; 6428 --mm2-accent: #6366f1; 6429 --mm2-accent-hover: #4f46e5; 6430 --mm2-accent-10: rgba(99,102,241,.10); 6431 --mm2-accent-20: rgba(99,102,241,.20); 6432 --mm2-fg: #0f172a; 6433 --mm2-fg-muted: #64748b; 6434 --mm2-fg-light: #94a3b8; 6435 6436 /* Enhanced Shadows - Multi-layer elevation */ 6437 --mm2-shadow-sm: 0 2px 8px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04); 6438 --mm2-shadow-md: 0 8px 24px rgba(15,23,42,.10), 0 2px 8px rgba(15,23,42,.06); 6439 --mm2-shadow-lg: 0 20px 60px rgba(15,23,42,.14), 0 8px 24px rgba(15,23,42,.08); 6440 --mm2-shadow-xl: 0 32px 80px rgba(15,23,42,.18), 0 12px 32px rgba(15,23,42,.10); 6441 --mm2-shadow-hover: 0 16px 40px rgba(15,23,42,.16), 0 6px 16px rgba(15,23,42,.08); 6442 6443 /* Border Radius */ 6444 --mm2-radius-xl: 20px; 6445 --mm2-radius-lg: 16px; 6446 --mm2-radius-md: 12px; 6447 --mm2-radius-sm: 10px; 6448 6449 --mm2-z: 60; 6450 6451 /* Animation Timing */ 6452 --mm2-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 6453 --mm2-transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); 6454 --mm2-transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); 6455} 6456 6457.navbar { position: relative; z-index: var(--mm2-z); } 6458 6459/* Trigger Button - Enhanced */ 6460.mm2-trigger { 6461 display: inline-flex; align-items: center; gap: 8px; 6462 padding: 10px 14px; border-radius: var(--mm2-radius-md); 6463 border: 1.5px solid var(--mm2-border); 6464 background: var(--mm2-surface); color: var(--mm2-fg); 6465 cursor: pointer; user-select: none; 6466 transition: all var(--mm2-transition-fast); 6467 text-decoration: none; 6468 font-weight: 600; 6469 box-shadow: var(--mm2-shadow-sm); 6470} 6471.mm2-trigger:hover { 6472 border-color: var(--mm2-accent); 6473 background: var(--mm2-muted); 6474 box-shadow: var(--mm2-shadow-md); 6475 transform: translateY(-1px); 6476} 6477.mm2-trigger:active { transform: translateY(0); } 6478.mm2-trigger svg {max-width: unset !important; transition: transform var(--mm2-transition-fast); } 6479.mm2-trigger:hover svg { transform: scale(1.05); } 6480 6481/* Backdrop - Enhanced Glassmorphism */ 6482.mm2-backdrop { 6483 position: fixed; 6484 inset: 0; 6485 top: 0; 6486 left: 0; 6487 right: 0; 6488 bottom: 0; 6489 background: rgba(15,23,42,.40); 6490 backdrop-filter: blur(8px) saturate(1.2); 6491 -webkit-backdrop-filter: blur(8px) saturate(1.2); 6492 opacity: 0; 6493 pointer-events: none; 6494 z-index: 499; 6495 transition: opacity var(--mm2-transition-base); 6496} 6497.mm2-open .mm2-backdrop { opacity: 1; pointer-events: auto; } 6498 6499/* Panel - Glassmorphism Enhanced */ 6500.mm2-panel { 6501 position: fixed; 6502 left: 50%; 6503 top: 120px; 6504 transform: translateX(-50%) translateY(-16px); 6505 width: min(1200px, 95vw); 6506 background: var(--mm2-surface-elevated); 6507 backdrop-filter: blur(20px) saturate(1.8); 6508 -webkit-backdrop-filter: blur(20px) saturate(1.8); 6509 border: 1.5px solid var(--mm2-border-strong); 6510 border-radius: var(--mm2-radius-xl); 6511 box-shadow: var(--mm2-shadow-xl); 6512 display: none; 6513 overflow: hidden; 6514 opacity: 0; 6515 z-index: 500; 6516 transition: transform var(--mm2-transition-slow), opacity var(--mm2-transition-base); 6517} 6518.mm2-open .mm2-panel { 6519 display: block; 6520 opacity: 1; 6521 transform: translateX(-50%) translateY(0); 6522} 6523 6524/* View switcher */ 6525.mm2-view { display: none; } 6526.mm2-open .mm2-view.is-active { display: block; } 6527 6528/* Top grid (level-1 tiles) - Enhanced */ 6529.mm2-top { 6530 padding: 24px; 6531 max-height: 75vh; overflow: auto; 6532} 6533.mm2-grid { 6534 display: grid; 6535 grid-template-columns: repeat(4, minmax(0,1fr)); 6536 gap: 14px; 6537} 6538@media (max-width: 1200px) { .mm2-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } } 6539@media (max-width: 960px) { .mm2-grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; } } 6540 6541/* Stagger animation for cards */ 6542.mm2-open .mm2-card { 6543 animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) backwards; 6544} 6545.mm2-open .mm2-card:nth-child(1) { animation-delay: 0.02s; } 6546.mm2-open .mm2-card:nth-child(2) { animation-delay: 0.04s; } 6547.mm2-open .mm2-card:nth-child(3) { animation-delay: 0.06s; } 6548.mm2-open .mm2-card:nth-child(4) { animation-delay: 0.08s; } 6549.mm2-open .mm2-card:nth-child(5) { animation-delay: 0.10s; } 6550.mm2-open .mm2-card:nth-child(6) { animation-delay: 0.12s; } 6551.mm2-open .mm2-card:nth-child(7) { animation-delay: 0.14s; } 6552.mm2-open .mm2-card:nth-child(8) { animation-delay: 0.16s; } 6553.mm2-open .mm2-card:nth-child(9) { animation-delay: 0.18s; } 6554.mm2-open .mm2-card:nth-child(10) { animation-delay: 0.20s; } 6555.mm2-open .mm2-card:nth-child(11) { animation-delay: 0.22s; } 6556.mm2-open .mm2-card:nth-child(12) { animation-delay: 0.24s; } 6557.mm2-open .mm2-card:nth-child(n+13) { animation-delay: 0.26s; } 6558 6559@keyframes slideInUp { 6560 from { 6561 opacity: 0; 6562 transform: translateY(12px); 6563 } 6564 to { 6565 opacity: 1;
6566 transform: translateY(0); 6567 } 6568} 6569 6570.mm2-card { 6571 position: relative; 6572 display: flex; gap: 14px; align-items: center; 6573 padding: 16px; border-radius: var(--mm2-radius-md); 6574 background: var(--mm2-surface); 6575 border: 1.5px solid var(--mm2-border); 6576 text-decoration: none; color: var(--mm2-fg); 6577 transition: all var(--mm2-transition-fast); 6578 min-height: 72px; 6579 box-shadow: var(--mm2-shadow-sm); 6580} 6581.mm2-card:hover { 6582 border-color: var(--mm2-accent); 6583 background: linear-gradient(135deg, rgba(255,255,255,1), rgba(248,250,252,1)); 6584 box-shadow: var(--mm2-shadow-hover); 6585 transform: translateY(-3px); 6586} 6587.mm2-card:active { transform: translateY(-1px); } 6588 6589/* Icon with category-specific gradients */ 6590.mm2-card .mm2-ico { 6591 width: 48px; height: 48px; border-radius: 12px; 6592 display: inline-flex; align-items: center; justify-content: center; 6593 background: linear-gradient(135deg, #e0e7ff, #fff); 6594 border: 1.5px solid #c7d2fe; 6595 color: #4f46e5; 6596 flex-shrink: 0; 6597 transition: all var(--mm2-transition-fast); 6598 box-shadow: 0 2px 8px rgba(99,102,241,0.1); 6599} 6600.mm2-card:hover .mm2-ico { 6601 transform: scale(1.08) rotate(2deg); 6602 box-shadow: 0 4px 16px rgba(99,102,241,0.2); 6603} 6604.mm2-card .mm2-ico svg { transition: transform var(--mm2-transition-fast); } 6605.mm2-card:hover .mm2-ico svg { transform: scale(1.1); } 6606 6607.mm2-card .mm2-title { 6608 font-weight: 700; 6609 font-size: 15px; 6610 letter-spacing: -0.01em; 6611 line-height: 1.3; 6612} 6613.mm2-card .mm2-sub { 6614 color: var(--mm2-fg-muted); 6615 font-size: 12.5px; 6616 margin-top: 3px; 6617 display: flex; 6618 align-items: center; 6619 gap: 4px; 6620 white-space: nowrap; 6621 overflow: hidden; 6622 text-overflow: ellipsis; 6623} 6624.mm2-card .mm2-count { 6625 display: inline-flex; 6626 padding: 2px 8px; 6627 border-radius: 6px; 6628 background: var(--mm2-accent-10); 6629 color: var(--mm2-accent); 6630 font-weight: 600; 6631 font-size: 11px; 6632 margin-left: 4px; 6633} 6634 6635/* Detail view (level-2 + level-3) - Enhanced */ 6636.mm2-detail { 6637 position: relative; 6638 padding: 16px 24px 24px; 6639 max-height: 70vh; overflow: auto; 6640} 6641.mm2-detail-header { 6642 display: flex; align-items: center; gap: 14px; 6643 padding: 10px 0 16px; border-bottom: 2px solid var(--mm2-muted); margin-bottom: 16px; 6644} 6645.mm2-back { 6646 display: inline-flex; align-items: center; justify-content: center; 6647 width: 40px; height: 40px; border-radius: 12px; 6648 border: 1.5px solid var(--mm2-border); background: #fff; color: var(--mm2-fg); 6649 cursor: pointer; 6650 transition: all var(--mm2-transition-fast); 6651 box-shadow: var(--mm2-shadow-sm); 6652} 6653.mm2-back:hover { 6654 border-color: var(--mm2-accent); 6655 background: var(--mm2-muted); 6656 transform: translateX(-3px); 6657 box-shadow: var(--mm2-shadow-md); 6658} 6659.mm2-detail-title { 6660 font-weight: 800; 6661 font-size: 18px; 6662 letter-spacing: -0.02em; 6663} 6664 6665/* Two-pane layout (desktop) - Enhanced */ 6666.mm2-detail-layout { 6667 display: grid; 6668 grid-template-columns: 280px 1fr; 6669 gap: 20px; 6670} 6671 6672/* Left: subnav (level-2 list) - Enhanced */ 6673.mm2-subnav { 6674 position: sticky; top: 8px; 6675 align-self: start; 6676 max-height: calc(70vh - 56px); 6677 overflow: auto; 6678 padding-right: 6px; 6679} 6680.mm2-subnav::-webkit-scrollbar { width: 8px; height: 8px; } 6681.mm2-subnav::-webkit-scrollbar-track { background: transparent; } 6682.mm2-subnav::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; } 6683.mm2-subnav::-webkit-scrollbar-thumb:hover { background: #94a3b8; } 6684 6685.mm2-subitem { 6686 display: flex; align-items: center; justify-content: flex-start; gap: 10px; 6687 width: 100%; 6688 padding: 12px 14px; 6689 border-radius: 12px; 6690 border: 1.5px solid transparent; 6691 background: transparent; 6692 color: var(--mm2-fg); 6693 cursor: pointer; 6694 text-align: left; 6695 transition: all var(--mm2-transition-fast); 6696 margin-bottom: 6px; 6697 font-weight: 600; 6698 font-size: 14px; 6699 position: relative; 6700} 6701.mm2-subitem::before { 6702 content: ''; 6703 position: absolute; 6704 left: 0; 6705 top: 50%; 6706 transform: translateY(-50%); 6707 width: 3px; 6708 height: 0; 6709 background: var(--mm2-accent); 6710 border-radius: 0 2px 2px 0; 6711 transition: height var(--mm2-transition-fast); 6712} 6713.mm2-subitem:hover { 6714 background: var(--mm2-muted); 6715 color: var(--mm2-accent-hover); 6716} 6717.mm2-subitem.active { 6718 background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(99,102,241,0.04)); 6719 border-color: var(--mm2-accent-20); 6720 color: var(--mm2-accent-hover); 6721 box-shadow: inset 0 0 0 1px rgba(99,102,241,0.1); 6722} 6723.mm2-subitem.active::before { 6724 height: 24px; 6725} 6726 6727/* Right: subcontent (level-3 grid) - Enhanced */ 6728.mm2-subcontent { min-height: 240px; } 6729.mm2-links { 6730 display: grid; gap: 12px 14px; 6731 grid-template-columns: repeat(3, minmax(0,1fr)); 6732} 6733@media (max-width: 1200px) { .mm2-links { grid-template-columns: repeat(2, minmax(0,1fr)); } } 6734 6735.mm2-link { 6736 display: flex; align-items: center; gap: 12px; 6737 padding: 14px; border: 1.5px solid var(--mm2-border); border-radius: var(--mm2-radius-md); 6738 background: var(--mm2-surface); color: var(--mm2-fg); text-decoration: none; 6739 min-height: 52px; 6740 font-weight: 500; 6741 font-size: 14px; 6742 transition: all var(--mm2-transition-fast); 6743 box-shadow: var(--mm2-shadow-sm); 6744 position: relative; 6745 justify-content: space-between; 6746} 6747.mm2-link:hover { 6748 background: var(--mm2-muted); 6749 border-color: var(--mm2-accent); 6750 transform: translateX(4px); 6751 box-shadow: var(--mm2-shadow-md); 6752} 6753.mm2-link-text { 6754 flex: 1; 6755} 6756 6757/* Product Thumbnails */ 6758.mm2-thumbs { 6759 display: flex; 6760 gap: 6px; 6761 align-items: center; 6762 margin-left: auto; 6763 flex-shrink: 0; 6764} 6765.mm2-thumb { 6766 width: 36px; 6767 height: 36px; 6768 border-radius: 8px; 6769 object-fit: cover; 6770 border: 1.5px solid var(--mm2-border); 6771 box-shadow: 0 2px 6px rgba(0,0,0,0.08); 6772 transition: all var(--mm2-transition-fast); 6773 background: var(--mm2-muted); 6774} 6775.mm2-link:hover .mm2-thumb { 6776 border-color: var(--mm2-accent); 6777 box-shadow: 0 4px 10px rgba(99,102,241,0.15); 6778 transform: scale(1.08); 6779} 6780
6781/* Thumbnail Skeleton Loader */ 6782.mm2-thumb-skeleton { 6783 width: 36px; 6784 height: 36px; 6785 border-radius: 8px; 6786 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 6787 background-size: 200% 100%; 6788 animation: shimmer 1.5s infinite; 6789 border: 1.5px solid var(--mm2-border); 6790} 6791 6792.mm2-badge { 6793 font-size: 11px; padding: 3px 10px; margin-left: auto; 6794 border-radius: 12px; background: var(--mm2-accent-10); color: var(--mm2-accent); 6795 font-weight: 700; 6796 border: 1px solid var(--mm2-accent-20); 6797} 6798 6799/* "See all" highlight card - Enhanced */ 6800.mm2-seeall { 6801 border: 2px dashed var(--mm2-accent); 6802 background: linear-gradient(135deg, rgba(99,102,241,.08), rgba(99,102,241,.02)); 6803 font-weight: 700; 6804} 6805.mm2-seeall:hover { 6806 background: linear-gradient(135deg, rgba(99,102,241,.14), rgba(99,102,241,.06)); 6807 border-style: solid; 6808 transform: translateX(6px) scale(1.02); 6809} 6810 6811/* Mobile panel styles - Enhanced with drag handle & better touch targets */ 6812@media (max-width: 960px) { 6813 .mm2-panel { 6814 position: fixed; 6815 left: 0; 6816 right: 0; 6817 bottom: 0; 6818 top: auto; 6819 width: 100%; 6820 max-height: 88vh; 6821 transform: translateX(0) translateY(100%); 6822 border-radius: var(--mm2-radius-xl) var(--mm2-radius-xl) 0 0; 6823 border-bottom: none; 6824 } 6825 .mm2-open .mm2-panel { 6826 transform: translateX(0) translateY(0); z-index: 9999; 6827 } 6828 6829 /* Enhanced backdrop for mobile */ 6830 .mm2-backdrop { 6831 backdrop-filter: blur(6px) saturate(1.2); 6832 -webkit-backdrop-filter: blur(6px) saturate(1.2); 6833 } 6834 6835 /* Mobile close button - Soft glassmorphism */ 6836 .mm2-mobile-close { 6837 position: absolute; 6838 top: 4px; 6839 right: 8px; 6840 width: 28px; 6841 height: 28px; 6842 border-radius: 50%; 6843 border: 1px solid rgba(226, 232, 240, 0.6); 6844 background: rgba(255, 255, 255, 0.85); 6845 backdrop-filter: blur(12px) saturate(1.8); 6846 -webkit-backdrop-filter: blur(12px) saturate(1.8); 6847 color: var(--mm2-fg-muted); 6848 display: inline-flex;
6849 align-items: center; 6850 justify-content: center; 6851 cursor: pointer; 6852 transition: all var(--mm2-transition-fast); 6853 box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); 6854 z-index: 20; 6855 } 6856 .mm2-mobile-close:hover { 6857 background: rgba(254, 242, 242, 0.95); 6858 border-color: rgba(252, 165, 165, 0.8); 6859 color: #dc2626; 6860 box-shadow: 0 4px 12px rgba(220, 38, 38, 0.15); 6861 } 6862 .mm2-mobile-close:active { 6863 transform: scale(0.92); 6864 } 6865 6866 /* Drag handle indicator */ 6867 .mm2-top::before, 6868 .mm2-detail::before { 6869 content: ''; 6870 position: sticky; 6871 top: 0; 6872 left: 50%; 6873 transform: translateX(-50%); 6874 width: 40px; 6875 height: 4px; 6876 background: var(--mm2-border-strong); 6877 border-radius: 999px; 6878 margin: 12px auto 20px; 6879 display: block; 6880 z-index: 10; 6881 transition: background var(--mm2-transition-fast); 6882 } 6883 .mm2-panel:active .mm2-top::before, 6884 .mm2-panel:active .mm2-detail::before { 6885 background: #94a3b8; 6886 } 6887 6888 .mm2-top { 6889 padding: 10px; 6890 } 6891 6892 .mm2-top, .mm2-detail { 6893 max-height: min(82vh, 720px); 6894 padding-top: 8px; 6895 } 6896 6897 /* Two-pane collapses: subnav becomes GRID (no horizontal scroll) */ 6898 .mm2-detail-layout { display: block; } 6899 .mm2-subnav { 6900 position: static; max-height: none; overflow: visible; 6901 display: grid; gap: 10px; padding: 10px 4px 16px; margin-bottom: 16px; 6902 grid-template-columns: repeat(2, minmax(0,1fr)); 6903 border-bottom: 2px solid var(--mm2-muted); 6904 } 6905 .mm2-subitem { 6906 margin: 0; width: 100%; text-align: center; 6907 border-radius: 14px; 6908 background: #fff; border: 1.5px solid var(--mm2-border); 6909 white-space: normal; line-height: 1.3; min-height: 48px; 6910 justify-content: center; 6911 padding: 12px 10px; 6912 font-size: 13.5px; 6913 box-shadow: var(--mm2-shadow-sm); 6914 } 6915 .mm2-subitem::before { display: none; } 6916 .mm2-subitem:hover { 6917 transform: scale(1.02); 6918 } 6919 .mm2-subitem.active { 6920 background: linear-gradient(135deg, var(--mm2-accent), var(--mm2-accent-hover)); 6921 border-color: var(--mm2-accent-hover); 6922 color: white; 6923 box-shadow: 0 4px 12px rgba(99,102,241,0.3); 6924 font-weight: 700; 6925 } 6926 6927 /* Level-3 grid tetap 2 kolom dengan spacing lebih baik */ 6928 .mm2-links { 6929 grid-template-columns: repeat(2, minmax(0,1fr)); 6930 gap: 10px; 6931 } 6932 6933 /* Mobile: hide thumbnails untuk save space */ 6934 .mm2-thumbs { 6935 display: none; 6936 } 6937 6938 /* Larger touch targets for mobile */ 6939 .mm2-card { 6940 min-height: 80px; 6941 padding: 18px 14px; 6942 } 6943 .mm2-card .mm2-ico { 6944 width: 50px; 6945 height: 50px; 6946 } 6947 .mm2-card .mm2-title { 6948 font-size: 14px; 6949 } 6950 .mm2-card .mm2-sub { 6951 font-size: 11.5px; 6952 } 6953 .mm2-link { 6954 min-height: 56px; 6955 padding: 16px; 6956 } 6957 .mm2-back { 6958 width: 44px; 6959 height: 44px; 6960 } 6961 6962 /* Grid spacing */ 6963 .mm2-grid { 6964 gap: 10px; 6965 } 6966} 6967</style> 6968 <style data-inlined="product-compare.css"> 6969/* Product Compare Feature Styles */ 6970 6971/* Compare Button in Product Detail - Modern Chip Style */ 6972.pd-compare-section { 6973 margin: 0.5rem 0; 6974 display: flex; 6975 align-items: center; 6976 gap: 0.5rem; 6977} 6978 6979.pd-compare-btn { 6980 display: inline-flex; 6981 align-items: center; 6982 gap: 0.4rem; 6983 padding: 0.5rem 0.85rem; 6984 border: 1px solid #cbd5e1; 6985 border-radius: 20px; 6986 background: #fff; 6987 color: #64748b; 6988 font-weight: 600; 6989 font-size: 0.8rem; 6990 cursor: pointer; 6991 transition: all 0.2s ease; 6992 position: relative; 6993 white-space: nowrap; 6994} 6995 6996.pd-compare-btn:hover { 6997 border-color: #3b82f6; 6998 color: #3b82f6; 6999 background: #f0f9ff; 7000 transform: translateY(-1px); 7001 box-shadow: 0 2px 6px rgba(59, 130, 246, 0.15); 7002} 7003 7004.pd-compare-btn.added { 7005 border-color: #16a34a; 7006 color: #16a34a; 7007 background: #f0fdf4; 7008} 7009 7010.pd-compare-btn .compare-remove { 7011 display: none; 7012} 7013 7014.pd-compare-btn.added .compare-text { 7015 display: none; 7016} 7017 7018.pd-compare-btn.added .compare-remove { 7019 display: flex !important; 7020 align-items: center; 7021 gap: 0.25rem; 7022 font-weight: 600; 7023 font-size: 0.9rem; 7024 color: #dc2626 !important; 7025 opacity: 1 !important; 7026 visibility: visible !important; 7027 z-index: 1; 7028 position: relative; 7029} 7030 7031.pd-compare-btn.added .compare-remove[hidden] { 7032 display: flex !important; 7033} 7034 7035/* Force visibility for added state */ 7036.pd-compare-btn.added .compare-remove { 7037 display: flex !important; 7038} 7039 7040[hidden].compare-remove { 7041 display: none !important; 7042} 7043 7044.pd-compare-btn.added [hidden].compare-remove { 7045 display: flex !important; 7046} 7047 7048/* Ensure the added state text is always visible */ 7049.pd-compare-btn.added { 7050 border-color: #dc2626 !important; 7051 color: #dc2626 !important; 7052 background: #fef2f2 !important; 7053} 7054 7055.pd-compare-btn.added .compare-remove svg { 7056 color: #dc2626 !important; 7057} 7058 7059.pd-compare-btn.added:hover { 7060 border-color: #15803d; 7061 color: #15803d; 7062 background: #dcfce7; 7063 transform: translateY(-1px); 7064 box-shadow: 0 2px 6px rgba(22, 163, 74, 0.2); 7065} 7066 7067.pd-compare-btn .compare-icon { 7068 width: 16px; 7069 height: 16px; 7070 flex-shrink: 0; 7071} 7072 7073.pd-compare-info { 7074 display: inline-flex; 7075 align-items: center; 7076} 7077 7078.compare-note { 7079 color: #94a3b8; 7080 font-size: 0.7rem; 7081 font-weight: 500; 7082 transition: color 0.2s ease; 7083} 7084 7085.pd-compare-btn.added + .pd-compare-info .compare-note { 7086 color: #16a34a; 7087 font-weight: 600; 7088} 7089 7090/* Compare Badge in Navigation */ 7091.compare-indicator { 7092 position: relative; 7093} 7094 7095.compare-indicator .badge { 7096 position: absolute; 7097 top: -8px; 7098 right: -8px; 7099 min-width: 18px;
7100 height: 18px; 7101 padding: 0 5px; 7102 background: #ef4444; 7103 color: white; 7104 border-radius: 9px; 7105 font-size: 11px; 7106 7107 7108/* Floating Compare Badge (Sticky) */ 7109.compare-floating-badge { 7110 position: fixed; 7111 right: 16px; 7112 bottom: calc(20px + 60px); /* Above scroll-to-top button */ 7113 width: auto; 7114 min-width: 56px; 7115 height: 56px; 7116 padding: 0 16px; 7117 border-radius: 28px; 7118 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); 7119 color: #fff; 7120 box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 0.5); 7121 cursor: pointer; 7122 z-index: 1104; 7123 opacity: 0; 7124 transform: translateY(8px) scale(0.9); 7125 transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.2s ease; 7126 display: flex; 7127 align-items: center; 7128 justify-content: center; 7129 gap: 8px; 7130 font-weight: 600; 7131 font-size: 0.85rem; 7132 pointer-events: none; 7133} 7134 7135.compare-floating-badge.visible { 7136 opacity: 1; 7137 transform: translateY(0) scale(1); 7138 pointer-events: auto; 7139} 7140 7141.compare-floating-badge:hover { 7142 box-shadow: 0 12px 32px -10px rgba(59, 130, 246, 0.6); 7143 transform: translateY(-2px) scale(1.02); 7144} 7145 7146.compare-floating-badge:active { 7147 transform: translateY(0) scale(0.98); 7148} 7149 7150.compare-floating-badge .cfb-icon { 7151 width: 20px; 7152 height: 20px; 7153 flex-shrink: 0; 7154} 7155 7156.compare-floating-badge .cfb-count { 7157 font-size: 0.9rem; 7158 font-weight: 700; 7159 min-width: 20px; 7160 text-align: center; 7161 background: rgba(255, 255, 255, 0.25); 7162 padding: 2px 8px; 7163 border-radius: 12px; 7164 line-height: 1; 7165 display: flex; 7166 align-items: center; 7167 justify-content: center; 7168 height: 24px; 7169} 7170 7171.compare-floating-badge .cfb-text { 7172 display: none; 7173} 7174 7175@media (min-width: 641px) { 7176 .compare-floating-badge .cfb-text { 7177 display: block; 7178 white-space: nowrap; 7179 } 7180} 7181 7182@media (max-width: 640px) { 7183 .compare-floating-badge { 7184 right: 14px; 7185 bottom: calc(var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))) + 60px); 7186 min-width: 48px; 7187 height: 48px; 7188 padding: 0 12px; 7189 border-radius: 24px; 7190 } 7191 7192 .compare-floating-badge .cfb-icon { 7193 width: 18px; 7194 height: 18px; 7195 } 7196 7197 .compare-floating-badge .cfb-count { 7198 font-size: 0.8rem; 7199 padding: 2px 6px; 7200 height: 20px; 7201 } 7202} font-weight: 600; 7203 display: flex; 7204 align-items: center; 7205 justify-content: center; 7206 transform: scale(0); 7207 transition: transform 0.2s ease; 7208} 7209 7210.compare-indicator .badge.active { 7211 transform: scale(1); 7212} 7213 7214/* Compare Sidebar */ 7215.compare-sidebar { 7216 position: fixed; 7217 top: 0; 7218 right: -400px; 7219 width: 400px; 7220 height: 100vh; 7221 background: #ffffff; 7222 box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); 7223 z-index: 9999; 7224 display: flex; 7225 flex-direction: column; 7226 transition: right 0.3s ease; 7227} 7228 7229.compare-sidebar.open { 7230 right: 0; 7231} 7232 7233.compare-overlay { 7234 position: fixed; 7235 top: 0; 7236 left: 0; 7237 width: 100%; 7238 height: 100%; 7239 background: rgba(0, 0, 0, 0.5); 7240 z-index: 9998; 7241 opacity: 0; 7242 visibility: hidden; 7243 transition: all 0.3s ease; 7244} 7245 7246.compare-overlay.active { 7247 opacity: 1; 7248 visibility: visible; 7249} 7250 7251.compare-header { 7252 display: flex; 7253 align-items: center; 7254 justify-content: space-between; 7255 padding: 1.25rem; 7256 border-bottom: 1px solid #e2e8f0; 7257 background: #f8fafc; 7258} 7259 7260.compare-title { 7261 display: flex; 7262 align-items: center; 7263 gap: 0.5rem; 7264 font-size: 1rem; 7265 font-weight: 600; 7266 color: #1e293b; 7267 margin: 0; 7268} 7269 7270.compare-count { 7271 color: #64748b; 7272 font-weight: 400; 7273} 7274 7275.compare-close { 7276 display: flex; 7277 align-items: center; 7278 justify-content: center; 7279 width: 32px; 7280 height: 32px; 7281 border: none; 7282 border-radius: 6px; 7283 background: transparent; 7284 color: #64748b; 7285 cursor: pointer; 7286 transition: all 0.2s ease; 7287} 7288 7289.compare-close:hover { 7290 background: #e2e8f0; 7291 color: #374151; 7292} 7293 7294.compare-content { 7295 flex: 1; 7296 overflow-y: auto; 7297 padding: 1rem; 7298} 7299 7300.compare-empty { 7301 display: flex; 7302 flex-direction: column; 7303 align-items: center; 7304 justify-content: center; 7305 height: 100%; 7306 text-align: center; 7307 padding: 2rem 1rem; 7308} 7309 7310.compare-empty h5 { 7311 margin: 1rem 0 0.5rem; 7312 color: #374151; 7313} 7314 7315.compare-empty p { 7316 color: #64748b; 7317 font-size: 0.875rem; 7318 line-height: 1.5; 7319} 7320 7321.compare-list { 7322 display: flex; 7323 flex-direction: column; 7324 gap: 0.75rem; 7325} 7326 7327.compare-item { 7328 display: flex; 7329 align-items: center; 7330 gap: 0.75rem; 7331 padding: 0.75rem; 7332 border: 1px solid #e2e8f0; 7333 border-radius: 8px; 7334 background: #ffffff; 7335 transition: all 0.2s ease; 7336} 7337 7338.compare-item:hover { 7339 border-color: #cbd5e1; 7340 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 7341} 7342 7343.compare-item-image { 7344 flex-shrink: 0; 7345 width: 48px; 7346 height: 48px; 7347 border-radius: 6px; 7348 overflow: hidden; 7349 border: 1px solid #e2e8f0; 7350} 7351 7352.compare-item-image img { 7353 width: 100%; 7354 height: 100%; 7355 object-fit: cover; 7356} 7357 7358.compare-item-info { 7359 flex: 1; 7360 min-width: 0; 7361} 7362 7363.compare-item-name { 7364 font-weight: 500; 7365 font-size: 0.875rem; 7366 color: #1e293b; 7367 margin-bottom: 0.25rem; 7368 line-height: 1.3; 7369 display: -webkit-box; 7370 -webkit-line-clamp: 2; 7371 -webkit-box-orient: vertical; 7372 overflow: hidden; 7373} 7374 7375.compare-item-price { 7376 font-weight: 600; 7377 font-size: 0.8rem; 7378 color: #059669; 7379} 7380 7381.compare-item-remove { 7382 flex-shrink: 0; 7383 display: flex; 7384 align-items: center; 7385 justify-content: center; 7386 width: 28px; 7387 height: 28px; 7388 border: none; 7389 border-radius: 4px; 7390 background: transparent; 7391 color: #dc2626; 7392 cursor: pointer; 7393 transition: all 0.2s ease; 7394} 7395 7396.compare-item-remove:hover { 7397 background: #fef2f2; 7398 color: #991b1b; 7399} 7400 7401.compare-actions { 7402 padding: 1rem; 7403 border-top: 1px solid #e2e8f0; 7404 display: flex; 7405 gap: 0.75rem; 7406} 7407 7408.compare-actions .btn { 7409 flex: 1; 7410 padding: 0.75rem 1rem; 7411 border-radius: 6px; 7412 font-weight: 600; 7413 font-size: 0.875rem; 7414 display: flex; 7415 align-items: center; 7416 justify-content: center; 7417 gap: 0.5rem; 7418 cursor: pointer; 7419 transition: all 0.2s ease; 7420} 7421 7422.btn-outline-danger { 7423 border: 1px solid #dc2626; 7424 background: transparent; 7425 color: #dc2626; 7426} 7427 7428.btn-outline-danger:hover { 7429 background: #dc2626; 7430 color: white; 7431} 7432 7433.btn-primary { 7434 border: 1px solid #3b82f6; 7435 background: #3b82f6; 7436 color: white; 7437} 7438 7439.btn-primary:hover { 7440 background: #2563eb; 7441 border-color: #2563eb; 7442} 7443 7444.btn-primary:disabled { 7445 opacity: 0.5;
7446 cursor: not-allowed; 7447} 7448 7449.btn-primary:disabled:hover { 7450 background: #3b82f6; 7451 border-color: #3b82f6; 7452} 7453 7454/* AI Comparison Modal */ 7455.ai-compare-modal { 7456 position: fixed; 7457 top: 0; 7458 left: 0; 7459 width: 100%; 7460 height: 100%; 7461 z-index: 10000; 7462 display: none; 7463 align-items: center; 7464 justify-content: center; 7465} 7466 7467.ai-compare-modal.open { 7468 display: flex; 7469} 7470 7471.ai-compare-modal .modal-backdrop { 7472 position: absolute; 7473 top: 0; 7474 left: 0; 7475 width: 100%; 7476 height: 100%; 7477 background: rgba(0, 0, 0, 0.6); 7478 backdrop-filter: blur(4px); 7479} 7480 7481.modal-container { 7482 position: relative; 7483 width: 90%; 7484 max-width: 1000px; 7485 max-height: 90vh; 7486 background: #ffffff; 7487 border-radius: 12px; 7488 box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); 7489 display: flex; 7490 flex-direction: column; 7491 overflow: hidden; 7492} 7493 7494.modal-header { 7495 display: flex; 7496 align-items: center; 7497 justify-content: space-between; 7498 padding: 1.5rem 2rem; 7499 border-bottom: 1px solid #e2e8f0; 7500 background: #f8fafc; 7501} 7502 7503.modal-title { 7504 display: flex; 7505 align-items: center; 7506 gap: 0.75rem; 7507 font-size: 1.25rem; 7508 font-weight: 600; 7509 color: #1e293b; 7510 margin: 0; 7511} 7512 7513.modal-close { 7514 display: flex; 7515 align-items: center; 7516 justify-content: center; 7517 width: 36px; 7518 height: 36px; 7519 border: none; 7520 border-radius: 8px; 7521 background: transparent; 7522 color: #64748b; 7523 cursor: pointer; 7524 transition: all 0.2s ease; 7525} 7526 7527.modal-close:hover { 7528 background: #e2e8f0; 7529 color: #374151; 7530} 7531 7532.modal-content { 7533 flex: 1; 7534 overflow-y: auto; 7535 padding: 2rem; 7536} 7537 7538/* AI Loading State */ 7539.ai-loading { 7540 display: flex; 7541 flex-direction: column; 7542 align-items: center; 7543 justify-content: center; 7544 padding: 3rem 2rem; 7545 text-align: center; 7546} 7547 7548.loading-animation { 7549 margin-bottom: 1rem; 7550} 7551 7552.typing-indicator { 7553 display: flex; 7554 align-items: center; 7555 justify-content: center; 7556 gap: 4px; 7557 margin-bottom: 1rem; 7558} 7559 7560.typing-indicator span { 7561 width: 8px; 7562 height: 8px; 7563 background: #3b82f6; 7564 border-radius: 50%; 7565 animation: typing 1.4s infinite ease-in-out; 7566} 7567 7568.typing-indicator span:nth-child(2) { 7569 animation-delay: 0.2s; 7570} 7571 7572.typing-indicator span:nth-child(3) { 7573 animation-delay: 0.4s; 7574} 7575 7576@keyframes typing { 7577 0%, 80%, 100% { 7578 opacity: 0.3; 7579 transform: scale(0.8); 7580 } 7581 40% { 7582 opacity: 1; 7583 transform: scale(1); 7584 } 7585} 7586 7587.ai-loading p { 7588 font-weight: 500; 7589 color: #1e293b; 7590 margin-bottom: 0.5rem; 7591} 7592 7593.ai-loading small { 7594 color: #64748b; 7595} 7596 7597/* AI Error State */ 7598.ai-error { 7599 display: flex; 7600 flex-direction: column; 7601 align-items: center; 7602 justify-content: center; 7603 padding: 3rem 2rem; 7604 text-align: center; 7605} 7606 7607.ai-error h4 { 7608 margin: 1rem 0 0.5rem; 7609 color: #dc2626; 7610} 7611 7612.ai-error p { 7613 color: #64748b; 7614 margin-bottom: 1.5rem; 7615} 7616 7617/* AI Results */ 7618.ai-results > div { 7619 margin-bottom: 2rem; 7620} 7621 7622.ai-summary-card, 7623.ai-comparison-table, 7624.ai-detailed-analysis, 7625.ai-recommendations, 7626.ai-tips { 7627 background: #f8fafc; 7628 border: 1px solid #e2e8f0; 7629 border-radius: 12px; 7630 padding: 1.5rem; 7631} 7632 7633.ai-results h4 { 7634 display: flex; 7635 align-items: center; 7636 gap: 0.5rem; 7637 font-size: 1.1rem; 7638 font-weight: 600; 7639 color: #1e293b; 7640 margin-bottom: 1rem; 7641} 7642 7643.summary-text { 7644 font-size: 1rem; 7645 line-height: 1.6; 7646 color: #374151; 7647 margin-bottom: 1rem; 7648} 7649 7650.winner-badge { 7651 padding: 1rem; 7652 background: linear-gradient(135deg, #10b981 0%, #059669 100%); 7653 color: white; 7654 border-radius: 8px; 7655} 7656 7657.winner-badge strong { 7658 display: block; 7659 margin-bottom: 0.25rem; 7660} 7661 7662.winner-badge small { 7663 opacity: 0.9; 7664 font-size: 0.875rem; 7665} 7666 7667/* Comparison Table */ 7668.comparison-grid { 7669 display: grid; 7670 gap: 1rem; 7671 margin-top: 1rem; 7672} 7673 7674.comparison-category { 7675 background: white; 7676 border: 1px solid #e2e8f0; 7677 border-radius: 8px; 7678 overflow: hidden; 7679} 7680 7681.category-header { 7682 background: #f1f5f9; 7683 padding: 0.75rem 1rem; 7684 font-weight: 600; 7685 color: #1e293b; 7686 border-bottom: 1px solid #e2e8f0; 7687} 7688 7689.category-products { 7690 display: grid; 7691 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 7692 gap: 1px; 7693 background: #e2e8f0; 7694} 7695 7696.product-score { 7697 background: white; 7698 padding: 1rem; 7699 text-align: center; 7700} 7701 7702.score-value { 7703 display: inline-flex; 7704 align-items: center; 7705 gap: 0.25rem; 7706 margin-bottom: 0.5rem; 7707} 7708 7709.score-stars { 7710 color: #fbbf24; 7711} 7712 7713.score-note { 7714 font-size: 0.8rem; 7715 color: #64748b; 7716 line-height: 1.3; 7717} 7718 7719/* Product name header in comparison table */ 7720.product-name-header { 7721 margin-bottom: 0.5rem; 7722} 7723 7724.product-name-header strong { 7725 font-size: .8rem; /* h5 size (18px) */ 7726 font-weight: 600; 7727 color: #1e293b; 7728 line-height: 1.3; 7729 display: block; 7730 word-break: break-word; 7731} 7732 7733.product-price { 7734 font-size: 0.8rem; 7735 font-weight: 600; 7736 color: #059669; 7737} 7738 7739/* Detailed Analysis */ 7740.products-analysis { 7741 display: grid; 7742 gap: 1rem; 7743 margin-top: 1rem; 7744} 7745 7746.product-analysis { 7747 background: white; 7748 border: 1px solid #e2e8f0; 7749 border-radius: 8px; 7750 padding: 1rem; 7751} 7752 7753.product-analysis h5 { 7754 color: #1e293b; 7755 margin-bottom: 0.75rem; 7756 font-weight: 600; 7757} 7758 7759.analysis-section { 7760 margin-bottom: 0.75rem; 7761} 7762 7763.analysis-section:last-child { 7764 margin-bottom: 0; 7765} 7766 7767.analysis-section h6 { 7768 font-size: 0.875rem; 7769 font-weight: 600; 7770 margin-bottom: 0.5rem; 7771} 7772 7773.analysis-section h6.strengths { 7774 color: #059669; 7775} 7776 7777.analysis-section h6.weaknesses { 7778 color: #dc2626; 7779} 7780 7781.analysis-section h6.best-for { 7782 color: #7c3aed; 7783} 7784 7785.analysis-section ul { 7786 list-style: none; 7787 padding: 0; 7788 margin: 0; 7789} 7790 7791.analysis-section li { 7792 display: flex; 7793 align-items: flex-start; 7794 gap: 0.5rem; 7795 margin-bottom: 0.25rem; 7796 font-size: 0.875rem; 7797 line-height: 1.4; 7798} 7799 7800.analysis-section li:before { 7801 content: "â¢"; 7802 color: #64748b; 7803 font-weight: bold; 7804 margin-top: 0.1rem; 7805} 7806 7807.best-for-text { 7808 font-size: 0.875rem; 7809 color: #374151; 7810 font-style: italic; 7811} 7812 7813/* Recommendations */ 7814.recommendations-grid { 7815 display: grid; 7816 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 7817 gap: 1rem; 7818 margin-top: 1rem; 7819} 7820 7821.recommendation-card { 7822 background: white; 7823 border: 1px solid #e2e8f0; 7824 border-radius: 8px; 7825 padding: 1rem; 7826 text-align: center; 7827} 7828 7829.recommendation-card h5 { 7830 font-size: 1rem; 7831 margin-bottom: 0.75rem; 7832 color: #1e293b; 7833} 7834 7835.recommendation-card p { 7836 font-size: 0.875rem; 7837 color: #374151; 7838 line-height: 1.5; 7839} 7840 7841/* Tips */ 7842.tips-list { 7843 list-style: none; 7844 padding: 0; 7845 margin: 1rem 0 0 0; 7846} 7847 7848.tips-list li { 7849 display: flex; 7850 align-items: flex-start; 7851 gap: 0.75rem; 7852 margin-bottom: 0.75rem; 7853 font-size: 0.9rem; 7854 line-height: 1.5; 7855 color: #374151; 7856} 7857 7858.tips-list li:before { 7859 content: "ð¡"; 7860 font-size: 1rem; 7861 margin-top: 0.1rem; 7862} 7863 7864/* Modal Footer */ 7865.modal-footer { 7866 display: flex; 7867 justify-content: space-between; 7868 align-items: center; 7869 padding: 1.5rem 2rem; 7870 border-top: 1px solid #e2e8f0; 7871 background: #f8fafc; 7872} 7873 7874.btn-outline-secondary { 7875 border: 1px solid #64748b; 7876 background: transparent; 7877 color: #64748b; 7878 padding: 0.75rem 1.5rem; 7879 border-radius: 6px; 7880 font-weight: 600; 7881 display: flex; 7882 align-items: center; 7883 gap: 0.5rem; 7884 cursor: pointer; 7885 transition: all 0.2s ease; 7886} 7887 7888.btn-outline-secondary:hover { 7889 background: #64748b; 7890 color: white; 7891} 7892 7893/* Responsive for chip button */ 7894@media (max-width: 880px) { 7895 .pd-compare-section { 7896 margin: 1rem 0; 7897 display: block; 7898 } 7899 7900 .pd-compare-btn { 7901 width: 100%; 7902 font-size: 0.85rem; 7903 padding: 0.65rem 1rem; 7904 border-radius: 10px; 7905 justify-content: center; 7906 min-height: 44px;
7907 } 7908 7909 .pd-compare-btn .compare-icon { 7910 width: 16px; 7911 height: 16px; 7912 } 7913 7914 .pd-compare-info { 7915 margin-top: 0.5rem; 7916 display: block; 7917 text-align: center; 7918 } 7919 7920 .compare-note { 7921 font-size: 0.7rem; 7922 display: block; 7923 } 7924} 7925 7926/* Responsive Design */ 7927@media (max-width: 768px) { 7928 .compare-sidebar { 7929 width: 100%; 7930 right: -100%; 7931 /* Fix for mobile viewport height changes */ 7932 height: 100vh; 7933 height: 100dvh; /* Dynamic viewport height for modern browsers */ 7934 } 7935 7936 .compare-actions { 7937 /* Ensure actions are always visible */ 7938 position: sticky; 7939 bottom: 0; 7940 background: white; 7941 border-top: 2px solid #e2e8f0; 7942 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); 7943 z-index: 10; 7944 } 7945 7946 .compare-actions .btn { 7947 /* Larger touch targets for mobile */ 7948 min-height: 48px; 7949 font-size: 0.9rem; 7950 font-weight: 700; 7951 } 7952 7953 .modal-container { 7954 width: 95%; 7955 max-height: 95vh; 7956 max-height: 95dvh; /* Dynamic viewport height */ 7957 } 7958 7959 .modal-header, 7960 .modal-content, 7961 .modal-footer { 7962 padding: 1rem; 7963 } 7964 7965 .recommendations-grid { 7966 grid-template-columns: 1fr; 7967 } 7968 7969 .category-products { 7970 grid-template-columns: 1fr; 7971 } 7972 7973 .modal-footer { 7974 flex-direction: column; 7975 gap: 0.75rem; 7976 } 7977 7978 .modal-footer .btn { 7979 width: 100%; 7980 } 7981 7982 /* Ensure compare button is visible on mobile */ 7983 .pd-compare-section { 7984 display: block !important; 7985 visibility: visible !important; 7986 opacity: 1 !important; 7987 margin: 1rem 0; 7988 padding: 0; 7989 position: relative !important; 7990 z-index: 1 !important; 7991 max-width: 100%; 7992 overflow: hidden; 7993 } 7994 7995 .pd-compare-btn { 7996 display: flex !important; 7997 visibility: visible !important; 7998 opacity: 1 !important; 7999 width: 100%; 8000 padding: 0.75rem 1rem; 8001 font-size: 0.9rem; 8002 min-height: 48px; /* Minimum touch target for mobile */ 8003 border: 2px solid #e2e8f0 !important; 8004 background: #fff !important; 8005 position: relative !important; 8006 z-index: 2 !important; 8007 box-sizing: border-box; 8008 white-space: normal !important; /* Allow text wrapping on mobile */ 8009 flex-wrap: wrap; 8010 gap: 0.5rem; 8011 } 8012 8013 .pd-compare-btn .compare-text { 8014 display: inline !important; 8015 visibility: visible !important; 8016 font-weight: 600; 8017 color: #475569 !important; 8018 } 8019 8020 .pd-compare-btn .compare-remove { 8021 display: none !important; /* Hidden by default */ 8022 } 8023 8024 .pd-compare-btn.added .compare-remove { 8025 display: flex !important; /* Only show when added */ 8026 flex-wrap: wrap; 8027 align-items: center; 8028 gap: 0.4rem; 8029 white-space: normal !important; 8030 word-break: break-word; 8031 } 8032 8033 .pd-compare-btn .compare-icon { 8034 display: inline-block !important; 8035 width: 18px; 8036 height: 18px; 8037 flex-shrink: 0; 8038 } 8039 8040 .pd-compare-info { 8041 margin-top: 0.5rem; 8042 } 8043 8044 .compare-note { 8045 font-size: 0.85rem !important; 8046 display: block !important; 8047 visibility: visible !important; 8048 color: #64748b !important; 8049 } 8050} 8051 8052/* iOS Safari specific fixes */ 8053@media (max-width: 480px) { 8054 #compareBtn { 8055 width:46px; 8056 height:46px; 8057 } 8058 8059 .pd-compare-section { 8060 -webkit-transform: translateZ(0); 8061 transform: translateZ(0); 8062 } 8063 8064 .pd-compare-btn { 8065 -webkit-appearance: none !important; 8066 appearance: none !important; 8067 -webkit-tap-highlight-color: transparent; 8068 touch-action: manipulation; 8069 } 8070 8071 /* Fix sidebar height for iOS address bar */ 8072 .compare-sidebar { 8073 /* Use fixed positioning that accounts for iOS address bar */ 8074 height: 100vh; 8075 height: -webkit-fill-available; /* iOS Safari compatibility */ 8076 min-height: 100vh; 8077 min-height: -webkit-fill-available; 8078 } 8079 8080 /* Ensure compare actions stay at bottom */ 8081 .compare-actions { 8082 position: sticky; 8083 bottom: 0; 8084 background: white; 8085 border-top: 2px solid #e2e8f0; 8086 box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); 8087 padding: 1.25rem 1rem; 8088 z-index: 20; 8089 } 8090 8091 .compare-actions .btn { 8092 min-height: 52px; /* Larger for easier tapping */ 8093 font-size: 1rem; 8094 font-weight: 700; 8095 border-radius: 8px; 8096 } 8097 8098 /* Adjust content padding to account for sticky actions */ 8099 .compare-content { 8100 padding-bottom: 6rem; /* Extra space for sticky actions */ 8101 } 8102} 8103 8104/* High-resolution mobile devices (iPhone 12 Pro, etc.) */ 8105@media (-webkit-min-device-pixel-ratio: 2) and (max-width: 430px) { 8106 .pd-compare-section { 8107 display: block !important; 8108 } 8109 8110 .pd-compare-btn { 8111 display: flex !important; 8112 min-height: 44px;
8113 font-size: 16px; /* Prevent zoom on iOS */ 8114 } 8115 8116 /* Enhanced fixes for iPhone 12 Pro and similar devices */ 8117 .compare-sidebar { 8118 /* Ensure full height coverage */ 8119 height: 100vh; 8120 height: 100dvh; /* Dynamic viewport height */ 8121 height: -webkit-fill-available; /* iOS fallback */ 8122 max-height: 100vh; 8123 max-height: 100dvh; 8124 max-height: -webkit-fill-available; 8125 overflow: hidden; /* Prevent scroll issues */ 8126 } 8127 8128 .compare-content { 8129 /* Flexible content area */ 8130 flex: 1; 8131 overflow-y: auto; 8132 -webkit-overflow-scrolling: touch; 8133 padding: 1rem 1rem 8rem 1rem; /* Extra bottom padding */ 8134 } 8135 8136 .compare-actions { 8137 /* Force actions to stay visible */ 8138 position: fixed; 8139 bottom: 0; 8140 left: 0; 8141 right: 0; 8142 width: 100%; 8143 background: white; 8144 border-top: 2px solid #e2e8f0; 8145 box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); 8146 padding: 1rem; 8147 z-index: 9999; 8148 display: flex !important; 8149 gap: 0.75rem; 8150 } 8151 8152 .compare-actions .btn { 8153 flex: 1; 8154 min-height: 30px; /* Extra large for high-DPI screens */ 8155 font-size: .8rem; 8156 font-weight: 600; 8157 border-radius: 12px; 8158 display: flex !important; 8159 align-items: center; 8160 justify-content: center; 8161 gap: 0.5rem; 8162 } 8163 8164 /* Ensure AI analyze button is always visible and prominent */ 8165 #compareAnalyze { 8166 background: #3b82f6 !important; 8167 border-color: #3b82f6 !important; 8168 color: white !important; 8169 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 8170 } 8171 8172 #compareAnalyze:not(:disabled) { 8173 background: #2563eb !important; 8174 border-color: #2563eb !important; 8175 transform: scale(1.02); 8176 } 8177 8178 #compareAnalyze:disabled { 8179 opacity: 0.6 !important; 8180 background: #9ca3af !important; 8181 border-color: #9ca3af !important; 8182 } 8183} 8184 8185/* AI Disclaimer Styles */ 8186.ai-disclaimer { 8187 display: flex; 8188 align-items: flex-start; 8189 gap: 0.75rem; 8190 padding: 1rem; 8191 background: #fef3c7; 8192 border: 1px solid #fbbf24; 8193 border-radius: 8px; 8194 margin: 0; 8195} 8196 8197.disclaimer-icon { 8198 font-size: 1.25rem; 8199 flex-shrink: 0; 8200 margin-top: 0.125rem; 8201} 8202 8203.disclaimer-text { 8204 font-size: 0.85rem; 8205 line-height: 1.4; 8206 color: #92400e; 8207} 8208 8209.disclaimer-text strong { 8210 color: #78350f; 8211 font-weight: 600; 8212} 8213 8214/* Compare Disclaimer in Sidebar */ 8215.compare-disclaimer { 8216 margin-top: 1rem; 8217 padding: 1rem; 8218 background: #f0f9ff; 8219 border: 1px solid #0ea5e9; 8220 border-radius: 8px; 8221} 8222 8223.compare-disclaimer h6 { 8224 display: flex; 8225 align-items: center; 8226 gap: 0.5rem; 8227 font-size: 0.875rem; 8228 font-weight: 600; 8229 color: #0c4a6e; 8230 margin-bottom: 0.5rem; 8231} 8232 8233.compare-disclaimer p { 8234 font-size: 0.8rem; 8235 line-height: 1.4; 8236 color: #075985; 8237 margin-bottom: 0.5rem; 8238} 8239 8240.compare-disclaimer p:last-child { 8241 margin-bottom: 0; 8242} 8243 8244.compare-disclaimer ul { 8245 list-style: none; 8246 padding: 0; 8247 margin: 0.5rem 0 0 0; 8248} 8249 8250.compare-disclaimer li { 8251 display: flex; 8252 align-items: flex-start; 8253 gap: 0.5rem; 8254 margin-bottom: 0.25rem; 8255 font-size: 0.75rem; 8256 line-height: 1.3; 8257 color: #075985; 8258} 8259 8260.compare-disclaimer li:before { 8261 content: "â¢"; 8262 color: #0ea5e9; 8263 font-weight: bold; 8264 margin-top: 0.1rem; 8265 flex-shrink: 0; 8266} 8267 8268/* Mobile Browser Address Bar Fixes */ 8269@supports (height: 100dvh) { 8270 /* Modern browsers with dynamic viewport support */ 8271 @media (max-width: 768px) { 8272 .compare-sidebar { 8273 height: 100dvh !important; 8274 max-height: 100dvh !important; 8275 } 8276 8277 .modal-container { 8278 max-height: 95dvh !important; 8279 } 8280 } 8281} 8282 8283@supports (height: -webkit-fill-available) { 8284 /* iOS Safari specific support */ 8285 @media (max-width: 768px) { 8286 .compare-sidebar { 8287 height: -webkit-fill-available !important; 8288 max-height: -webkit-fill-available !important; 8289 } 8290 } 8291} 8292 8293/* Fallback for older browsers */ 8294@media (max-width: 768px) and (orientation: portrait) { 8295 .compare-sidebar { 8296 /* Fixed height that works across different mobile browsers */ 8297 min-height: 600px; 8298 height: calc(100vh - env(keyboard-inset-height, 0px)); 8299 height: calc(100vh - env(safe-area-inset-bottom, 0px)); 8300 } 8301 8302 /* Ensure actions are always accessible */ 8303 .compare-actions { 8304 position: sticky !important; 8305 bottom: env(safe-area-inset-bottom, 0px) !important; 8306 margin-bottom: 0 !important; 8307 padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)) !important; 8308 } 8309} 8310 8311/* PWA and fullscreen app support */ 8312@media (display-mode: standalone) { 8313 .compare-sidebar { 8314 height: 100vh !important; 8315 height: 100dvh !important; 8316 } 8317} 8318 8319/* Utilities */
8320.btn-sm { 8321 padding: 0.5rem 0.75rem; 8322 font-size: 0.8rem; 8323} 8324 8325[hidden] { 8326 display: none !important; 8327} 8328 8329/* ===== UX ENHANCEMENTS ===== */ 8330 8331/* Compare Button Highlight Animation */ 8332.pulse-highlight { 8333 animation: pulse-glow 2s ease-in-out 3; 8334 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8335} 8336 8337@keyframes pulse-glow { 8338 0% { 8339 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8340 } 8341 50% { 8342 box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); 8343 } 8344 100% { 8345 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); 8346 } 8347} 8348 8349/* Flying Product Animation */ 8350.flying-product { 8351 position: fixed; 8352 z-index: 9999; 8353 pointer-events: none; 8354} 8355 8356.flying-product img { 8357 border-radius: 8px; 8358 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 8359} 8360 8361/* Sticky Compare Bar - REMOVED (using only fly-to-navbar animation) */ 8362 8363/* Inline Compare Preview - REMOVED (keeping it simple and clean) */ 8364</style> 8365 <style data-inlined="product-card-actions.css"> 8366/** 8367 * Product Card Quick Actions - 3 Dot Menu + Bottom Sheet 8368 * Tokopedia-inspired design 8369 */ 8370 8371/* ===== 3 Dot Button on Product Card ===== */ 8372.prod-card { 8373 position: relative; 8374} 8375 8376.prod-card-actions-btn { 8377 position: absolute; 8378 top: 8px; 8379 right: 8px; 8380 width: 32px; 8381 height: 32px; 8382 border-radius: 50%; 8383 background: rgba(255, 255, 255, 0.95); 8384 border: 1px solid rgba(0, 0, 0, 0.08); 8385 display: inline-flex;
8386 align-items: center; 8387 justify-content: center; 8388 cursor: pointer; 8389 z-index: 3; 8390 transition: all 0.2s ease; 8391 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 8392 -webkit-tap-highlight-color: transparent; 8393} 8394 8395.prod-card-actions-btn:hover { 8396 background: #fff; 8397 border-color: rgba(0, 0, 0, 0.15); 8398 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 8399 transform: scale(1.05); 8400} 8401 8402.prod-card-actions-btn:active { 8403 transform: scale(0.95); 8404} 8405 8406.prod-card-actions-btn svg { 8407 width: 20px; 8408 height: 20px; 8409 color: #64748b; 8410 display: block; 8411} 8412 8413/* Desktop: Show on hover */ 8414@media (min-width: 641px) { 8415 .prod-card-actions-btn { 8416 opacity: 0; 8417 pointer-events: none; 8418 } 8419 8420 .prod-card:hover .prod-card-actions-btn { 8421 opacity: 1; 8422 pointer-events: auto; 8423 } 8424} 8425 8426/* Mobile: Always visible but smaller */ 8427@media (max-width: 640px) { 8428 .prod-card-actions-btn { 8429 width: 28px; 8430 height: 28px; 8431 top: 6px; 8432 right: 6px; 8433 opacity: 95%; 8434 } 8435 8436 .prod-card-actions-btn svg { 8437 width: 18px; 8438 height: 18px; 8439 } 8440} 8441 8442/* ===== Bottom Sheet Overlay ===== */ 8443.pca-overlay { 8444 position: fixed; 8445 inset: 0; 8446 background: rgba(15, 23, 42, 0.6); 8447 backdrop-filter: blur(4px); 8448 z-index: 99998; 8449 opacity: 0; 8450 visibility: hidden; 8451 transition: all 0.3s ease; 8452} 8453 8454.pca-overlay.active { 8455 opacity: 1; 8456 visibility: visible; 8457} 8458 8459/* ===== Bottom Sheet Container ===== */ 8460.pca-bottom-sheet { 8461 position: fixed; 8462 bottom: 0; 8463 left: 0; 8464 right: 0; 8465 background: #fff; 8466 border-radius: 20px 20px 0 0; 8467 box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15); 8468 z-index: 99999; 8469 max-width: 600px; 8470 margin: 0 auto; 8471 transform: translateY(100%); 8472 transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 8473} 8474 8475.pca-bottom-sheet.active { 8476 transform: translateY(0); 8477} 8478 8479/* Drag Handle */ 8480.pca-sheet-handle { 8481 width: 40px; 8482 height: 4px; 8483 background: #cbd5e1; 8484 border-radius: 999px; 8485 margin: 12px auto 8px; 8486} 8487 8488/* Product Preview */ 8489.pca-sheet-product { 8490 display: flex; 8491 gap: 12px; 8492 padding: 12px 20px; 8493 border-bottom: 1px solid #f1f5f9; 8494} 8495 8496.pca-product-image { 8497 width: 60px; 8498 height: 60px; 8499 border-radius: 8px; 8500 object-fit: cover; 8501 flex-shrink: 0; 8502 border: 1px solid #e2e8f0; 8503} 8504 8505.pca-product-info { 8506 flex: 1; 8507 min-width: 0; 8508} 8509 8510.pca-product-name { 8511 font-size: 14px; 8512 font-weight: 600; 8513 color: #0f172a; 8514 margin: 0 0 4px 0; 8515 display: -webkit-box; 8516 -webkit-line-clamp: 2; 8517 -webkit-box-orient: vertical; 8518 overflow: hidden; 8519 line-height: 1.3; 8520} 8521 8522.pca-product-price { 8523 font-size: 15px; 8524 font-weight: 700; 8525 color: #0ea5e9; 8526 margin: 0; 8527} 8528 8529.pca-product-stock { 8530 font-size: 12px; 8531 color: #64748b; 8532 margin: 2px 0 0 0; 8533} 8534 8535.pca-product-stock.in { 8536 color: #16a34a; 8537} 8538 8539.pca-product-stock.low { 8540 color: #ea580c; 8541} 8542 8543.pca-product-stock.out { 8544 color: #dc2626; 8545} 8546 8547/* Action Menu */ 8548.pca-sheet-actions { 8549 padding: 8px 0 20px; 8550} 8551 8552.pca-action-item { 8553 display: flex;
8554 align-items: center; 8555 gap: 16px; 8556 padding: 16px 20px; 8557 background: transparent; 8558 border: 0; 8559 width: 100%; 8560 cursor: pointer; 8561 transition: background-color 0.15s ease; 8562 -webkit-tap-highlight-color: transparent; 8563} 8564 8565.pca-action-item:hover { 8566 background: #f8fafc; 8567} 8568 8569.pca-action-item:active { 8570 background: #f1f5f9; 8571} 8572 8573.pca-action-item:disabled { 8574 opacity: 0.4; 8575 cursor: not-allowed; 8576} 8577 8578.pca-action-item:disabled:hover { 8579 background: transparent; 8580} 8581 8582.pca-action-icon { 8583 width: 24px; 8584 height: 24px; 8585 color: #475569; 8586 flex-shrink: 0; 8587} 8588 8589.pca-action-content { 8590 flex: 1; 8591 text-align: left; 8592} 8593 8594.pca-action-label { 8595 font-size: 15px; 8596 font-weight: 600; 8597 color: #0f172a; 8598 margin: 0 0 2px 0; 8599} 8600 8601.pca-action-desc { 8602 font-size: 12px; 8603 color: #64748b; 8604 margin: 0; 8605} 8606 8607.pca-action-badge { 8608 display: inline-flex; 8609 align-items: center; 8610 justify-content: center; 8611 min-width: 20px; 8612 height: 20px; 8613 padding: 0 6px; 8614 background: #0ea5e9; 8615 color: #fff; 8616 font-size: 11px; 8617 font-weight: 700; 8618 border-radius: 999px; 8619 margin-left: auto; 8620} 8621 8622.pca-action-badge.compare { 8623 background: #8b5cf6; 8624} 8625 8626/* Action Item Colors */ 8627.pca-action-item.cart .pca-action-icon { 8628 color: #0ea5e9; 8629} 8630 8631.pca-action-item.compare .pca-action-icon { 8632 color: #8b5cf6; 8633} 8634 8635.pca-action-item.compare.active { 8636 background: rgba(139, 92, 246, 0.05); 8637} 8638 8639.pca-action-item.compare.active .pca-action-label { 8640 color: #8b5cf6; 8641} 8642 8643.pca-action-item.compare.active .pca-action-icon { 8644 color: #8b5cf6; 8645} 8646 8647.pca-action-item.share .pca-action-icon { 8648 color: #64748b; 8649} 8650 8651/* Loading State */ 8652.pca-action-item.loading { 8653 pointer-events: none; 8654} 8655 8656.pca-action-item.loading .pca-action-icon { 8657 animation: spin 1s linear infinite; 8658} 8659 8660@keyframes spin { 8661 from { transform: rotate(0deg); } 8662 to { transform: rotate(360deg); } 8663} 8664 8665/* Success State Animation */ 8666.pca-action-item.success .pca-action-icon { 8667 animation: checkmark 0.5s ease; 8668} 8669 8670@keyframes checkmark { 8671 0% { transform: scale(0); } 8672 50% { transform: scale(1.2); } 8673 100% { transform: scale(1); } 8674} 8675 8676/* ===== Safe Area for Mobile Devices ===== */ 8677@supports (padding-bottom: env(safe-area-inset-bottom)) { 8678 .pca-sheet-actions { 8679 padding-bottom: calc(20px + env(safe-area-inset-bottom)); 8680 } 8681} 8682 8683/* ===== Responsive Tweaks ===== */ 8684@media (max-width: 640px) { 8685 .pca-bottom-sheet { 8686 border-radius: 16px 16px 0 0; 8687 } 8688 8689 .pca-sheet-product { 8690 padding: 10px 16px; 8691 } 8692 8693 .pca-product-image { 8694 width: 50px; 8695 height: 50px; 8696 } 8697 8698 .pca-product-name { 8699 font-size: 13px; 8700 } 8701 8702 .pca-product-price { 8703 font-size: 14px; 8704 } 8705 8706 .pca-action-item { 8707 padding: 14px 16px; 8708 gap: 12px; 8709 } 8710 8711 .pca-action-icon { 8712 width: 22px; 8713 height: 22px; 8714 } 8715 8716 .pca-action-label { 8717 font-size: 14px; 8718 } 8719} 8720 8721/* ===== Animation for Flying Product ===== */ 8722.flying-product { 8723 position: fixed; 8724 pointer-events: none; 8725 z-index: 99999; 8726 animation: fadeOut 0.8s ease forwards; 8727} 8728 8729@keyframes fadeOut { 8730 to { 8731 opacity: 0; 8732 transform: scale(0.5); 8733 } 8734} 8735 8736/* ===== Desktop Dropdown (Alternative) ===== */ 8737@media (min-width: 1024px) { 8738 /* Optional: Could add desktop dropdown menu here */ 8739 /* For now, using bottom sheet for consistency */ 8740} 8741 8742/* ===== Print Styles ===== */ 8743@media print { 8744 .prod-card-actions-btn, 8745 .pca-overlay, 8746 .pca-bottom-sheet { 8747 display: none !important; 8748 } 8749} 8750 8751</style> 8752 <style data-inlined="modal.css"> 8753/* Modern Modal Dialog for Cart Operations */ 8754.modal-overlay { 8755 position: fixed; 8756 top: 0; 8757 left: 0; 8758 right: 0; 8759 bottom: 0; 8760 background: rgba(0, 0, 0, 0.5); 8761 backdrop-filter: blur(4px); 8762 display: flex; 8763 align-items: center; 8764 justify-content: center; 8765 z-index: 9999; 8766 opacity: 0; 8767 visibility: hidden; 8768 transition: all 0.2s ease; 8769} 8770 8771.modal-overlay.show { 8772 opacity: 1;
8773 visibility: visible; 8774} 8775 8776.modal-dialog { 8777 background: white; 8778 border-radius: 16px; 8779 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); 8780 max-width: 400px; 8781 width: 90%; 8782 max-height: 90vh; 8783 overflow-y: auto; 8784 transform: scale(0.95); 8785 transition: transform 0.2s ease; 8786} 8787 8788.modal-overlay.show .modal-dialog { 8789 transform: scale(1); 8790} 8791 8792.modal-header { 8793 padding: 24px 24px 16px; 8794 border-bottom: 1px solid #f1f5f9; 8795} 8796 8797.modal-title { 8798 font-size: 20px; 8799 font-weight: 700; 8800 color: #0f172a; 8801 margin: 0; 8802 display: flex; 8803 align-items: center; 8804 gap: 12px; 8805} 8806 8807.modal-body { 8808 padding: 16px 24px 24px; 8809} 8810 8811.modal-message { 8812 font-size: 16px; 8813 line-height: 1.5; 8814 color: #475569; 8815 margin: 0; 8816} 8817 8818.modal-actions { 8819 display: flex; 8820 gap: 12px; 8821 margin-top: 24px; 8822} 8823 8824.modal-btn { 8825 flex: 1; 8826 padding: 12px 16px; 8827 border: none; 8828 border-radius: 8px; 8829 font-size: 14px; 8830 font-weight: 600; 8831 cursor: pointer; 8832 transition: all 0.2s ease; 8833 text-align: center; 8834} 8835 8836.modal-btn-secondary { 8837 background: #f8fafc; 8838 color: #64748b; 8839 border: 1px solid #e2e8f0; 8840} 8841 8842.modal-btn-secondary:hover { 8843 background: #f1f5f9; 8844 border-color: #cbd5e1; 8845} 8846 8847.modal-btn-danger { 8848 background: #ef4444; 8849 color: white; 8850} 8851 8852.modal-btn-danger:hover { 8853 background: #dc2626; 8854} 8855 8856.modal-btn-primary { 8857 background: #3b82f6; 8858 color: white; 8859} 8860 8861.modal-btn-primary:hover { 8862 background: #2563eb; 8863} 8864 8865.modal-icon { 8866 font-size: 24px; 8867 width: 32px; 8868 height: 32px; 8869 display: flex; 8870 align-items: center; 8871 justify-content: center; 8872 border-radius: 8px; 8873} 8874 8875.modal-icon.danger { 8876 background: #fef2f2; 8877 color: #ef4444; 8878} 8879 8880.modal-icon.warning { 8881 background: #fffbeb; 8882 color: #f59e0b; 8883} 8884 8885.modal-icon.success { 8886 background: #f0fdf4; 8887 color: #22c55e; 8888} 8889 8890.modal-icon.info { 8891 background: #eff6ff; 8892 color: #3b82f6; 8893} 8894 8895/* Mobile responsive */ 8896@media (max-width: 640px) { 8897 .modal-dialog { 8898 margin: 16px; 8899 max-width: none; 8900 width: auto; 8901 } 8902 8903 .modal-header, 8904 .modal-body { 8905 padding-left: 20px; 8906 padding-right: 20px; 8907 } 8908 8909 .modal-actions { 8910 flex-direction: column; 8911 } 8912 8913 .modal-btn { 8914 flex: none; 8915 } 8916} 8917</style> 8918 8919 <style data-inlined="reviews-points.css"> 8920/** 8921 * Reviews & Points System - Styles 8922 * Product reviews, rating display, and point redemption UI 8923 */ 8924 8925/* =================================== 8926 PRODUCT REVIEWS 8927 =================================== */ 8928 8929.product-tabs { 8930 display: flex; 8931 border-bottom: 2px solid #e5e7eb; 8932 margin: 24px 0 16px; 8933 gap: 8px; 8934} 8935 8936.tab-btn { 8937 padding: 12px 20px; 8938 border: none; 8939 background: none; 8940 cursor: pointer; 8941 font-size: 15px; 8942 font-weight: 500; 8943 color: #6b7280; 8944 border-bottom: 3px solid transparent; 8945 transition: all 0.2s ease; 8946 position: relative; 8947 top: 2px; 8948} 8949 8950.tab-btn:hover { 8951 color: #111827; 8952} 8953 8954.tab-btn.active { 8955 color: #2563eb; 8956 border-bottom-color: #2563eb; 8957} 8958 8959.tab-content { 8960 padding: 20px 0; 8961} 8962 8963/* Review Statistics */ 8964.review-stats { 8965 display: grid; 8966 grid-template-columns: 200px 1fr; 8967 gap: 32px; 8968 padding: 24px; 8969 background: #f9fafb; 8970 border-radius: 12px; 8971 margin-bottom: 24px; 8972} 8973 8974.rating-summary { 8975 text-align: center; 8976} 8977 8978.avg-rating { 8979 display: block; 8980 font-size: 48px; 8981 font-weight: 700; 8982 color: #111827; 8983 line-height: 1; 8984} 8985 8986.rating-summary .stars { 8987 font-size: 24px; 8988 color: #fbbf24; 8989 margin: 8px 0; 8990} 8991 8992.total-reviews { 8993 display: block; 8994 font-size: 14px; 8995 color: #6b7280; 8996 margin-top: 4px; 8997} 8998 8999.rating-breakdown { 9000 display: flex; 9001 flex-direction: column; 9002 gap: 8px; 9003} 9004 9005.rating-bar { 9006 display: flex; 9007 align-items: center; 9008 gap: 12px; 9009} 9010 9011.rating-label { 9012 font-size: 14px; 9013 color: #374151; 9014 width: 32px; 9015 text-align: right; 9016} 9017 9018.bar-container { 9019 flex: 1; 9020 height: 8px; 9021 background: #e5e7eb; 9022 border-radius: 4px; 9023 overflow: hidden; 9024} 9025 9026.bar-fill { 9027 height: 100%; 9028 background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%); 9029 transition: width 0.3s ease; 9030} 9031 9032.rating-count { 9033 font-size: 14px; 9034 color: #6b7280; 9035 width: 40px; 9036 text-align: right; 9037} 9038 9039/* Review Filters */ 9040.review-filters { 9041 display: flex; 9042 justify-content: space-between; 9043 align-items: center; 9044 margin-bottom: 20px; 9045} 9046 9047.review-sort { 9048 padding: 8px 12px; 9049 border: 1px solid #d1d5db; 9050 border-radius: 8px; 9051 font-size: 14px; 9052 background: white; 9053} 9054 9055.write-review-btn { 9056 padding: 10px 20px; 9057 background: #2563eb; 9058 color: white; 9059 border: none; 9060 border-radius: 8px; 9061 font-weight: 500;
9062 cursor: pointer; 9063 transition: background 0.2s; 9064} 9065 9066.write-review-btn:hover { 9067 background: #1d4ed8; 9068} 9069 9070/* Review List */ 9071.reviews-list { 9072 display: flex; 9073 flex-direction: column; 9074 gap: 16px; 9075} 9076 9077.review-item { 9078 padding: 20px; 9079 background: white; 9080 border: 1px solid #e5e7eb; 9081 border-radius: 12px; 9082 transition: box-shadow 0.2s; 9083} 9084 9085.review-item:hover { 9086 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 9087} 9088 9089.review-header { 9090 display: flex; 9091 justify-content: space-between; 9092 align-items: flex-start; 9093 margin-bottom: 8px; 9094} 9095 9096.review-author { 9097 display: flex; 9098 align-items: center; 9099 gap: 8px; 9100} 9101 9102.review-author strong { 9103 font-size: 15px; 9104 color: #111827; 9105} 9106 9107.verified-badge { 9108 display: inline-flex; 9109 align-items: center; 9110 gap: 4px; 9111 font-size: 12px; 9112 color: #059669; 9113 background: #d1fae5; 9114 padding: 2px 8px; 9115 border-radius: 4px; 9116 font-weight: 500; 9117} 9118 9119.anonymous-badge { 9120 display: inline-flex; 9121 align-items: center; 9122 font-size: 12px; 9123 color: #6b7280; 9124 background: #f3f4f6; 9125 padding: 2px 6px; 9126 border-radius: 4px; 9127 margin-left: 4px; 9128 cursor: help; 9129} 9130 9131.review-rating .stars { 9132 font-size: 16px; 9133 color: #fbbf24; 9134} 9135 9136.review-date { 9137 font-size: 13px; 9138 color: #9ca3af; 9139 margin-bottom: 12px; 9140} 9141 9142.review-text { 9143 font-size: 15px; 9144 line-height: 1.6; 9145 color: #374151; 9146 margin-bottom: 12px; 9147} 9148 9149.review-images { 9150 display: flex; 9151 gap: 8px; 9152 margin-bottom: 12px; 9153 flex-wrap: wrap; 9154} 9155 9156.review-img { 9157 width: 80px; 9158 height: 80px; 9159 object-fit: cover; 9160 border-radius: 8px; 9161 cursor: pointer; 9162 transition: transform 0.2s; 9163} 9164 9165.review-img:hover { 9166 transform: scale(1.05); 9167} 9168 9169.review-actions { 9170 display: flex; 9171 gap: 12px; 9172 padding-top: 12px; 9173 border-top: 1px solid #f3f4f6; 9174} 9175 9176.btn-helpful { 9177 display: flex; 9178 align-items: center; 9179 gap: 6px; 9180 padding: 6px 12px; 9181 background: #f9fafb; 9182 border: 1px solid #e5e7eb; 9183 border-radius: 6px; 9184 font-size: 13px; 9185 color: #6b7280; 9186 cursor: pointer; 9187 transition: all 0.2s; 9188} 9189 9190.btn-helpful:hover { 9191 background: #f3f4f6; 9192 color: #111827; 9193 border-color: #d1d5db; 9194} 9195 9196.btn-helpful.voted { 9197 background: #dbeafe; 9198 color: #1d4ed8; 9199 border-color: #93c5fd; 9200} 9201 9202.helpful-icon { 9203 font-size: 14px; 9204} 9205 9206.no-reviews { 9207 text-align: center; 9208 padding: 60px 20px; 9209 color: #9ca3af; 9210 font-size: 15px; 9211} 9212 9213.load-more-btn { 9214 display: block; 9215 margin: 24px auto 0; 9216 padding: 12px 32px; 9217 background: white; 9218 border: 1px solid #d1d5db; 9219 border-radius: 8px; 9220 font-size: 14px; 9221 font-weight: 500; 9222 color: #374151; 9223 cursor: pointer; 9224 transition: all 0.2s; 9225} 9226 9227.load-more-btn:hover { 9228 background: #f9fafb; 9229 border-color: #9ca3af; 9230} 9231 9232/* =================================== 9233 POINT BALANCE WIDGET (LEGACY - kept for backward compatibility) 9234 =================================== */ 9235 9236.point-balance-card { 9237 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 9238 color: white; 9239 padding: 24px; 9240 border-radius: 16px; 9241 margin-bottom: 24px; 9242 box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); 9243} 9244 9245.point-header { 9246 display: flex; 9247 justify-content: space-between; 9248 align-items: center; 9249 margin-bottom: 12px; 9250} 9251 9252.point-header h3 { 9253 font-size: 18px; 9254 font-weight: 600; 9255 margin: 0; 9256 display: flex; 9257 align-items: center; 9258 gap: 8px; 9259} 9260 9261.point-header .balance { 9262 font-size: 32px; 9263 font-weight: 700; 9264} 9265 9266.point-value { 9267 font-size: 14px; 9268 opacity: 0.9; 9269 margin: 8px 0 16px; 9270} 9271 9272.point-stats { 9273 display: flex; 9274 gap: 20px; 9275 font-size: 13px; 9276 opacity: 0.85; 9277 margin-bottom: 16px; 9278} 9279 9280.btn-view-history { 9281 display: inline-block; 9282 padding: 10px 20px; 9283 background: rgba(255, 255, 255, 0.2); 9284 color: white; 9285 text-decoration: none; 9286 border-radius: 8px; 9287 font-size: 14px; 9288 font-weight: 500; 9289 transition: background 0.2s; 9290} 9291 9292.btn-view-history:hover { 9293 background: rgba(255, 255, 255, 0.3); 9294} 9295 9296/* =================================== 9297 CHECKOUT POINT REDEMPTION 9298 =================================== */ 9299 9300.checkout-points { 9301 padding: 16px; 9302 background: #fef3c7; 9303 border: 2px solid #fbbf24; 9304 border-radius: 12px; 9305 margin-bottom: 16px; 9306} 9307 9308.points-header { 9309 display: flex; 9310 align-items: center; 9311 gap: 10px; 9312 margin-bottom: 12px; 9313} 9314 9315.points-header input[type="checkbox"] { 9316 width: 18px;
9317 height: 18px; 9318 cursor: pointer; 9319} 9320 9321.points-header label { 9322 font-size: 15px; 9323 color: #78350f; 9324 cursor: pointer; 9325 margin: 0; 9326} 9327 9328#points-slider { 9329 padding: 16px; 9330 background: white; 9331 border-radius: 8px; 9332 margin-top: 12px; 9333} 9334 9335#points-slider label { 9336 display: block; 9337 font-size: 14px; 9338 color: #374151; 9339 margin-bottom: 8px; 9340 font-weight: 500; 9341} 9342 9343#points-amount { 9344 width: 100%; 9345 margin-bottom: 8px; 9346} 9347 9348#points-display { 9349 display: flex; 9350 justify-content: space-between; 9351 font-size: 14px; 9352 color: #111827; 9353 margin-top: 8px; 9354} 9355 9356#points-display .discount-amount { 9357 color: #059669; 9358 font-weight: 600; 9359} 9360 9361.price-row.point-discount { 9362 color: #059669; 9363 font-weight: 500; 9364} 9365 9366/* =================================== 9367 REVIEWABLE ORDERS 9368 =================================== */ 9369 9370.reviewable-orders { 9371 background: white; 9372 border: 1px solid #e5e7eb; 9373 border-radius: 12px; 9374 padding: 20px; 9375 margin-bottom: 24px; 9376} 9377 9378.reviewable-orders h3 { 9379 font-size: 18px; 9380 margin-bottom: 16px; 9381 color: #111827; 9382} 9383 9384.order-card { 9385 display: flex; 9386 gap: 16px; 9387 padding: 16px; 9388 background: #f9fafb; 9389 border-radius: 8px; 9390 margin-bottom: 12px; 9391} 9392 9393.order-card:last-child { 9394 margin-bottom: 0; 9395} 9396 9397.order-card img { 9398 width: 80px; 9399 height: 80px; 9400 object-fit: cover; 9401 border-radius: 8px; 9402} 9403 9404.order-info { 9405 flex: 1; 9406} 9407 9408.order-info h4 { 9409 font-size: 15px; 9410 margin: 0 0 4px; 9411 color: #111827; 9412} 9413 9414.order-info p { 9415 font-size: 13px; 9416 color: #6b7280; 9417 margin: 0 0 12px; 9418} 9419 9420.btn-review { 9421 padding: 8px 16px; 9422 background: #059669; 9423 color: white; 9424 border: none; 9425 border-radius: 6px; 9426 font-size: 14px; 9427 font-weight: 500; 9428 cursor: pointer; 9429 transition: background 0.2s; 9430} 9431 9432.btn-review:hover { 9433 background: #047857; 9434} 9435 9436/* =================================== 9437 TOAST NOTIFICATIONS 9438 =================================== */ 9439 9440.review-toast { 9441 position: fixed; 9442 bottom: 24px; 9443 right: 24px; 9444 padding: 14px 20px; 9445 background: #111827; 9446 color: white; 9447 border-radius: 8px; 9448 font-size: 14px; 9449 line-height: 1.4; 9450 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 9451 transform: translateY(100px); 9452 opacity: 0; 9453 transition: all 0.3s ease; 9454 z-index: 10000; 9455 max-width: 400px; 9456 word-wrap: break-word; 9457} 9458 9459.review-toast.show { 9460 transform: translateY(0); 9461 opacity: 1; 9462} 9463 9464/* Mobile: Position above bottom-nav */ 9465@media (max-width: 768px) { 9466 .review-toast { 9467 bottom: 80px; /* Above bottom-nav (50px height + padding + safe margin) */ 9468 right: 16px; 9469 left: 16px; 9470 max-width: calc(100% - 32px); 9471 } 9472} 9473 9474.review-toast.toast-success { 9475 background: #059669; 9476} 9477 9478.review-toast.toast-error { 9479 background: #dc2626; 9480} 9481 9482.review-toast.toast-info { 9483 background: #2563eb; 9484} 9485 9486/* =================================== 9487 RESPONSIVE 9488 =================================== */ 9489 9490@media (max-width: 768px) { 9491 .point-balance-card { 9492 border-radius: 0; 9493 } 9494 9495 .review-stats { 9496 grid-template-columns: 70px 1fr; 9497 gap: 20px; 9498 padding: 16px; 9499 } 9500 9501 .rating-summary { 9502 text-align: center; 9503 } 9504 9505 .rating-summary .avg-rating { 9506 font-size: 32px; 9507 } 9508 9509 .rating-summary .stars { 9510 font-size: 18px; 9511 margin: 4px 0; 9512 } 9513 9514 .total-reviews { 9515 font-size: 11px; 9516 } 9517 9518 /* Rating breakdown - more compact */ 9519 .rating-breakdown { 9520 gap: 2px; 9521 } 9522 9523 .rating-breakdown .rating-bar { 9524 gap: 4px; 9525 } 9526 9527 .rating-breakdown .rating-label { 9528 font-size: 11px; 9529 width: 16px; 9530 min-width: 16px; 9531 text-align: left; 9532 } 9533 9534 .rating-breakdown .bar-container { 9535 height: 6px; 9536 } 9537 9538 .rating-breakdown .rating-count { 9539 font-size: 11px; 9540 width: 16px; 9541 min-width: 16px; 9542 } 9543 9544 .review-header { 9545 flex-direction: column; 9546 gap: 8px; 9547 align-items: flex-start; 9548 } 9549 9550 .review-rating { 9551 order: -1; 9552 margin-bottom: 4px; 9553 } 9554 9555 .review-rating .stars { 9556 font-size: 14px; 9557 } 9558 9559 .review-filters { 9560 flex-direction: column; 9561 gap: 12px;
9562 align-items: stretch; 9563 } 9564 9565 .review-sort { 9566 width: 100%; 9567 } 9568 9569 .point-header .balance { 9570 font-size: 24px; 9571 } 9572 9573 .point-stats { 9574 flex-direction: column; 9575 gap: 8px; 9576 } 9577 9578 .order-card { 9579 flex-direction: column; 9580 } 9581 9582 .order-card img { 9583 width: 100%; 9584 height: 200px; 9585 } 9586} 9587 9588/* =================================== 9589 LOADING & ERROR STATES 9590 =================================== */ 9591 9592.review-loading { 9593 text-align: center; 9594 padding: 40px; 9595 color: #9ca3af; 9596 font-size: 14px; 9597} 9598 9599.review-error { 9600 text-align: center; 9601 padding: 40px; 9602 color: #dc2626; 9603 font-size: 14px; 9604 background: #fee2e2; 9605 border-radius: 8px; 9606} 9607 9608.review-empty { 9609 text-align: center; 9610 padding: 10px; 9611 color: #6b7280; 9612 font-size: 14px; 9613} 9614 9615/* =================================== 9616 REVIEWABLE ORDERS LIST 9617 =================================== */ 9618 9619.reviewable-orders-list { 9620 display: flex; 9621 flex-direction: column; 9622 gap: 16px; 9623} 9624 9625.reviewable-order-item { 9626 display: grid; 9627 grid-template-columns: 80px 1fr auto; 9628 gap: 16px; 9629 padding: 16px; 9630 background: #fff; 9631 border: 1px solid #e5e7eb; 9632 border-radius: 12px; 9633 align-items: center; 9634 transition: box-shadow 0.2s; 9635} 9636 9637.reviewable-order-item:hover { 9638 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 9639} 9640 9641.reviewable-order-item .roi-image { 9642 width: 80px; 9643 height: 80px; 9644 object-fit: cover; 9645 border-radius: 8px; 9646 border: 1px solid #e5e7eb; 9647} 9648 9649.reviewable-order-item .roi-info { 9650 display: flex; 9651 flex-direction: column; 9652 gap: 4px; 9653} 9654 9655.reviewable-order-item .roi-info h4 { 9656 margin: 0; 9657 font-size: 15px; 9658 font-weight: 600; 9659 color: #111827; 9660 line-height: 1.4; 9661} 9662 9663.reviewable-order-item .roi-meta { 9664 margin: 0; 9665 font-size: 13px; 9666 color: #6b7280; 9667} 9668 9669.reviewable-order-item .btn-write-review { 9670 white-space: nowrap; 9671 padding: 10px 20px; 9672 font-size: 14px; 9673} 9674 9675@media (max-width: 640px) { 9676 .reviewable-order-item { 9677 grid-template-columns: 60px 1fr; 9678 gap: 12px; 9679 } 9680 9681 .reviewable-order-item .roi-image { 9682 width: 60px; 9683 height: 60px; 9684 } 9685 9686 .reviewable-order-item .btn-write-review { 9687 grid-column: 1 / -1; 9688 width: 100%; 9689 } 9690} 9691 9692/* =================================== 9693 REVIEW SUBMISSION MODAL 9694 =================================== */ 9695 9696/* Modal Overlay Background */ 9697#review-modal-overlay { 9698 position: fixed; 9699 top: 0; 9700 left: 0; 9701 right: 0; 9702 bottom: 0; 9703 background: rgba(0, 0, 0, 0.6) !important; 9704 backdrop-filter: blur(8px); 9705 display: flex; 9706 align-items: center; 9707 justify-content: center; 9708 z-index: 99999 !important; 9709 padding: 20px; 9710} 9711 9712.review-modal { 9713 max-width: 600px !important; 9714 width: 90% !important; 9715 background: white; 9716 border-radius: 16px !important; 9717 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); 9718 max-height: 90vh; 9719 display: flex; 9720 flex-direction: column; 9721 overflow: hidden !important; 9722} 9723 9724#review-modal-overlay .modal-content, 9725.review-modal .modal-content { 9726 padding: 0 !important; 9727 flex: initial !important; 9728 overflow-y: visible !important; 9729} 9730 9731.review-modal .modal-header { 9732 display: flex; 9733 justify-content: space-between; 9734 align-items: center; 9735 padding: 20px 24px; 9736 border-bottom: 1px solid #e5e7eb; 9737} 9738 9739.review-modal .modal-title { 9740 margin: 0; 9741 font-size: 20px; 9742 font-weight: 700; 9743 color: #111827; 9744} 9745 9746.review-modal .modal-close { 9747 background: none; 9748 border: none; 9749 cursor: pointer; 9750 padding: 4px; 9751 color: #6b7280; 9752 transition: color 0.2s; 9753 display: flex; 9754 align-items: center; 9755 justify-content: center; 9756} 9757
9758.review-modal .modal-close:hover { 9759 color: #111827; 9760} 9761 9762.review-modal .modal-body { 9763 padding: 24px; 9764 max-height: calc(90vh - 140px); 9765 overflow-y: auto; 9766} 9767 9768/* Custom Scrollbar for Modal */ 9769.review-modal .modal-body::-webkit-scrollbar { 9770 width: 8px; 9771} 9772 9773.review-modal .modal-body::-webkit-scrollbar-track { 9774 background: #f1f5f9; 9775 border-radius: 4px; 9776} 9777 9778.review-modal .modal-body::-webkit-scrollbar-thumb { 9779 background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); 9780 border-radius: 4px; 9781} 9782 9783.review-modal .modal-body::-webkit-scrollbar-thumb:hover { 9784 background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%); 9785} 9786 9787/* Firefox Scrollbar */ 9788.review-modal .modal-body { 9789 scrollbar-width: thin; 9790 scrollbar-color: #3b82f6 #f1f5f9; 9791} 9792 9793.review-modal .modal-footer { 9794 display: flex; 9795 gap: 12px; 9796 margin-top: 24px; 9797 padding-top: 20px; 9798 border-top: 1px solid #f1f5f9; 9799} 9800 9801.review-product-info { 9802 display: flex; 9803 gap: 16px; 9804 padding: 16px; 9805 background: #f9fafb; 9806 border-radius: 8px; 9807 margin-bottom: 24px; 9808} 9809 9810.review-product-thumb { 9811 width: 80px; 9812 height: 80px; 9813 object-fit: cover; 9814 border-radius: 6px; 9815 border: 1px solid #e5e7eb; 9816} 9817 9818.review-product-details h4 { 9819 margin: 0 0 8px 0; 9820 font-size: 16px; 9821 font-weight: 600; 9822 color: #111827; 9823} 9824 9825.review-product-details p { 9826 margin: 0; 9827 font-size: 13px; 9828 color: #6b7280; 9829} 9830 9831/* Star Rating Input */ 9832.star-rating-input { 9833 display: inline-flex; 9834 gap: 8px; 9835 margin-right: 12px; 9836 vertical-align: middle; 9837} 9838 9839.star-btn { 9840 background: none; 9841 border: none; 9842 font-size: 32px; 9843 color: #d1d5db; 9844 cursor: pointer; 9845 transition: all 0.2s; 9846 padding: 0; 9847 line-height: 1; 9848} 9849 9850.star-btn:hover, 9851.star-btn.active { 9852 color: #fbbf24; 9853 transform: scale(1.1); 9854} 9855 9856.rating-label { 9857 display: inline-block; 9858 font-size: 15px; 9859 color: #6b7280; 9860 margin: 0; 9861 vertical-align: middle; 9862 min-width: 150px; 9863} 9864 9865/* Form Elements */ 9866.review-form .form-group { 9867 margin-bottom: 20px; 9868} 9869 9870.review-form .form-label { 9871 display: block; 9872 font-size: 14px; 9873 font-weight: 600; 9874 color: #374151; 9875 margin-bottom: 8px; 9876} 9877 9878.review-form .form-control { 9879 width: 100%; 9880 padding: 12px; 9881 border: 1px solid #d1d5db; 9882 border-radius: 8px; 9883 font-size: 14px; 9884 font-family: inherit; 9885 transition: border-color 0.2s; 9886} 9887 9888.review-form .form-control:focus { 9889 outline: none; 9890 border-color: #2563eb; 9891 box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 9892} 9893 9894.review-form .form-hint { 9895 font-size: 13px; 9896 color: #6b7280; 9897 margin-top: 8px; 9898} 9899 9900/* Image Upload */ 9901.btn-upload-trigger { 9902 display: inline-flex; 9903 align-items: center; 9904 gap: 8px; 9905 padding: 10px 16px; 9906 background: #f9fafb; 9907 border: 2px dashed #d1d5db; 9908 border-radius: 8px; 9909 color: #374151; 9910 cursor: pointer; 9911 font-size: 14px; 9912 font-weight: 500; 9913 transition: all 0.2s; 9914} 9915 9916.btn-upload-trigger:hover { 9917 background: #f3f4f6; 9918 border-color: #2563eb; 9919 color: #2563eb; 9920} 9921 9922.image-preview-grid { 9923 display: grid; 9924 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 9925 gap: 12px; 9926 margin-top: 16px; 9927} 9928 9929.image-preview-item { 9930 position: relative; 9931 aspect-ratio: 1; 9932 border-radius: 8px; 9933 overflow: hidden; 9934 border: 1px solid #e5e7eb; 9935} 9936 9937.image-preview-item img { 9938 width: 100%; 9939 height: 100%; 9940 object-fit: cover; 9941} 9942 9943.remove-image { 9944 position: absolute; 9945 top: 4px; 9946 right: 4px; 9947 background: rgba(0, 0, 0, 0.7); 9948 border: none; 9949 color: white; 9950 width: 24px; 9951 height: 24px; 9952 border-radius: 50%; 9953 cursor: pointer; 9954 display: flex; 9955 align-items: center; 9956 justify-content: center; 9957 transition: background 0.2s; 9958} 9959 9960.remove-image:hover { 9961 background: rgba(0, 0, 0, 0.9); 9962} 9963 9964/* Point Reward Info */ 9965.point-reward-info { 9966 background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); 9967 border: 2px solid #fbbf24; 9968 border-radius: 12px; 9969 padding: 16px; 9970 margin: 20px 0; 9971} 9972 9973.pri-header { 9974 display: flex; 9975 align-items: center; 9976 gap: 8px; 9977 font-size: 16px; 9978 font-weight: 700;
9979 color: #92400e; 9980 margin-bottom: 12px; 9981} 9982 9983.pri-list { 9984 list-style: none; 9985 padding: 0; 9986 margin: 0 0 12px 0; 9987} 9988 9989.pri-list li { 9990 padding: 6px 0; 9991 font-size: 14px; 9992 color: #78350f; 9993 transition: opacity 0.2s; 9994} 9995 9996.pri-total { 9997 padding-top: 12px; 9998 border-top: 2px solid rgba(251, 191, 36, 0.3); 9999 font-size: 16px; 10000 color: #92400e; 10001 font-weight: 600; 10002} 10003 10004.form-error { 10005 background: #fee2e2; 10006 border: 1px solid #f87171; 10007 color: #991b1b; 10008 padding: 12px; 10009 border-radius: 8px; 10010 font-size: 14px; 10011 margin-top: 16px; 10012} 10013 10014/* Buttons */ 10015.review-form .btn { 10016 padding: 12px 24px; 10017 border: none; 10018 border-radius: 8px; 10019 font-size: 15px; 10020 font-weight: 600; 10021 cursor: pointer; 10022 transition: all 0.2s; 10023} 10024 10025.review-form .btn-primary { 10026 background: #2563eb; 10027 color: white; 10028} 10029 10030.review-form .btn-primary:hover { 10031 background: #1d4ed8; 10032} 10033 10034.review-form .btn-primary:disabled { 10035 background: #94a3b8; 10036 cursor: not-allowed; 10037} 10038 10039.review-form .btn-secondary { 10040 background: #f1f5f9; 10041 color: #64748b; 10042} 10043 10044.review-form .btn-secondary:hover { 10045 background: #e2e8f0; 10046 color: #475569; 10047} 10048 10049@media (max-width: 768px) { 10050 #review-modal-overlay { padding: 0; } 10051 .review-modal { max-width: 100%; width: 100% !important; } 10052} 10053 10054/* =================================== 10055 POINT SUMMARY GRID (Profile Page) 10056 =================================== */ 10057 10058.point-summary-grid { 10059 display: grid; 10060 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 10061 gap: 12px; 10062 margin-bottom: 32px; 10063} 10064 10065.point-summary-card { 10066 background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); 10067 border: 1px solid #e5e7eb; 10068 border-radius: 12px; 10069 padding: 16px 20px; 10070 display: flex; 10071 align-items: center; 10072 justify-content: space-between; 10073 gap: 12px; 10074 transition: all 0.2s; 10075} 10076 10077.point-summary-card:hover { 10078 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 10079 transform: translateY(-2px); 10080} 10081 10082.point-summary-card .psc-left { 10083 display: flex; 10084 flex-direction: column; 10085 gap: 2px; 10086} 10087 10088.point-summary-card .psc-icon { 10089 font-size: 24px; 10090 margin-bottom: 4px; 10091} 10092 10093.point-summary-card .psc-label { 10094 font-size: 13px; 10095 color: #6b7280; 10096 font-weight: 500; 10097 text-transform: uppercase; 10098 letter-spacing: 0.5px; 10099} 10100 10101.point-summary-card .psc-value { 10102 font-size: 26px; 10103 font-weight: 700; 10104 color: #111827; 10105 flex-shrink: 0; 10106} 10107 10108/* Point History Section */ 10109.point-history-section { 10110 margin-top: 32px; 10111} 10112 10113.point-history-section h3 { 10114 font-size: 18px; 10115 font-weight: 600; 10116 color: #111827; 10117 margin-bottom: 16px; 10118} 10119 10120.point-history-list { 10121 display: flex; 10122 flex-direction: column; 10123 gap: 12px; 10124} 10125 10126.point-history-item { 10127 display: grid; 10128 grid-template-columns: 40px 1fr auto; 10129 gap: 16px; 10130 padding: 16px; 10131 background: white; 10132 border: 1px solid #e5e7eb; 10133 border-radius: 10px; 10134 align-items: center; 10135 transition: all 0.2s; 10136} 10137 10138.point-history-item:hover { 10139 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); 10140} 10141 10142.point-history-item.credit { 10143 border-left: 3px solid #10b981; 10144} 10145 10146.point-history-item.debit { 10147 border-left: 3px solid #f59e0b; 10148} 10149 10150.point-history-item .phi-icon { 10151 width: 40px; 10152 height: 40px; 10153 display: flex; 10154 align-items: center; 10155 justify-content: center; 10156 font-size: 20px; 10157 border-radius: 50%; 10158 background: #f9fafb; 10159} 10160 10161.point-history-item.credit .phi-icon { 10162 background: #d1fae5; 10163} 10164 10165.point-history-item.debit .phi-icon { 10166 background: #fef3c7; 10167} 10168
10169.point-history-item .phi-details { 10170 display: flex; 10171 flex-direction: column; 10172 gap: 4px; 10173} 10174 10175.point-history-item .phi-desc { 10176 font-size: 15px; 10177 font-weight: 500; 10178 color: #111827; 10179} 10180 10181.point-history-item .phi-date { 10182 font-size: 13px; 10183 color: #9ca3af; 10184} 10185 10186.point-history-item .phi-amount { 10187 font-size: 18px; 10188 font-weight: 700; 10189} 10190 10191.point-history-item .phi-amount.credit { 10192 color: #10b981; 10193} 10194 10195.point-history-item .phi-amount.debit { 10196 color: #f59e0b; 10197} 10198 10199@media (max-width: 640px) { 10200 .point-summary-grid { 10201 grid-template-columns: 1fr; 10202 } 10203 10204 .point-summary-card .psc-value { 10205 font-size: 22px; 10206 } 10207 10208 .point-history-item { 10209 grid-template-columns: 36px 1fr; 10210 gap: 12px; 10211 } 10212 10213 .point-history-item .phi-amount { 10214 grid-column: 2; 10215 text-align: right; 10216 margin-top: 8px; 10217 } 10218} 10219 10220/* ââ Pending points: compact widget indicator ââ */ 10221.pbc-pending { 10222 font-size: 12px; 10223 opacity: 0.85; 10224 margin-top: 4px; 10225 color: #fde68a; 10226} 10227 10228/* ââ Pending points: summary card ââ */ 10229.point-summary-card.psc-pending-card { 10230 background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); 10231 border-color: #fcd34d; 10232} 10233 10234.point-summary-card.psc-pending-card .psc-value { 10235 color: #b45309; 10236} 10237 10238.point-summary-card .psc-sub { 10239 font-size: 11px; 10240 color: #9ca3af; 10241 margin-top: 4px; 10242} 10243 10244/* ââ Pending points: history item ââ */ 10245.point-history-item.phi-pending { 10246 opacity: 0.75; 10247 border-left-color: #fcd34d; 10248 background: #fffbeb; 10249} 10250 10251.point-history-item.phi-pending .phi-icon { 10252 background: #fef3c7; 10253} 10254 10255.phi-pending-badge { 10256 display: inline-block; 10257 font-size: 11px; 10258 font-weight: 600; 10259 padding: 2px 7px; 10260 border-radius: 10px; 10261 background: #fef3c7; 10262 color: #92400e; 10263 border: 1px solid #fcd34d; 10264 margin-left: 8px; 10265 vertical-align: middle; 10266} 10267 10268/* ââ Reversed points: history item ââ */ 10269.point-history-item.phi-reversed { 10270 opacity: 0.55; 10271 border-left-color: #d1d5db; 10272 background: #f9fafb; 10273} 10274 10275.point-history-item.phi-reversed .phi-icon { 10276 background: #f3f4f6; 10277} 10278 10279.point-history-item.phi-reversed .phi-amount { 10280 text-decoration: line-through; 10281 color: #9ca3af !important; 10282} 10283 10284.phi-reversed-badge { 10285 display: inline-block; 10286 font-size: 11px; 10287 font-weight: 600; 10288 padding: 2px 7px; 10289 border-radius: 10px; 10290 background: #fee2e2; 10291 color: #991b1b; 10292 border: 1px solid #fca5a5; 10293 margin-left: 8px; 10294 vertical-align: middle; 10295} 10296 10297</style> 10298 10299</head> 10300<body> 10301<header class="topbar"> 10302 <div class="container topbar-inner"> 10303 <div class="topbar-left"> 10304 Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi 10305 </div> 10306 <div class="topbar-right"> 10307 <a href="https://calcourtsfitness.com/">PIONTOGEL</a> 10308 <a href="https://calcourtsfitness.com/">TOGEL ONLINE</a> 10309 <a href="https://calcourtsfitness.com/">TEBAK ANGKA</a> 10310 <a href="https://calcourtsfitness.com/">BANDAR TOGEL ONLINE</a> 10311 </div> 10312 </div> 10313</header> 10314<nav class="navbar"> 10315 <nav class="navbar-logo" style="width: 100%;"> 10316 <div class="container nav-inner" style="justify-items: center; justify-content: center;"> 10317 <a href="https://calcourtsfitness.com/" class="logo-mobile" title="PIONTOGEL"> 10318 <img src="https://a1.asdqwe.vip/assets/logo-piontogel.gif" alt="PIONTOGEL" height="40"> 10319 </a> 10320 </div> 10321</nav> 10322 <div class="container nav-inner"> 10323 <a href="https://calcourtsfitness.com/" class="logo" title="PIONTOGEL"> 10324 <img src="https://a1.asdqwe.vip/assets/logo-piontogel.gif" alt="PIONTOGEL" height="40"> 10325 </a> 10326 10327 10328 <form class="search" method="get" action="#"> 10329 <input type="hidden" name="page" value="produk"> 10330 10331 <!-- Search Icon (Desktop) --> 10332 <button type="submit" class="search-icon-btn" id="searchBtn" aria-label="Cari"> 10333 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 10334 <circle cx="11" cy="11" r="8"/> 10335 <path d="m21 21-4.35-4.35"/> 10336 </svg> 10337 </button> 10338 10339 <input type="text" name="q" id="searchInput" placeholder="Ketik diGoogle PIONTOGEL?" 10340 value="" autocomplete="off"> 10341 10342 <div class="search-suggest" id="searchSuggest" hidden></div> 10343 </form> 10344 10345 <div class="nav-actions"> 10346 <!--<button class="icon-btn" id="notifBtn" title="Notifikasi">ð</button>--> 10347 <button class="icon-btn cart-indicator" id="cartBtn" title="Keranjang"> 10348 ð <span id="cartCount" class="badge" data-role="cart-badge" aria-live="polite">0</span> 10349 </button> 10350 10351 <button class="icon-btn compare-indicator" id="compareBtn" title="Perbandingan"> 10352 âï¸ <span id="compareCount" class="badge" data-role="compare-badge" aria-live="polite">0</span> 10353 </button> 10354 10355 <!-- Dynamic login/logout button --> 10356 <button class="login-btn" id="loginBtn" aria-label="Masuk">
10357 <span class="login-text">Masuk</span> 10358 <span class="login-ico" aria-hidden="true">ð</span> 10359 </button> 10360 </div> 10361 </div> 10362 10363 <div class="container" style="display: flex; align-items: center;"> 10364 10365 10366 <div class="mega-cats-wrapper"> 10367 <div class="mega-cats container" id="megaCatsScroll" style="padding-left: 5px !important;"> 10368 <a class="" href="https://calcourtsfitness.com/"> 10369 PIONTOGEL </a> 10370 <a class="" href="https://calcourtsfitness.com/"> 10371 PIONTOGEL LOGIN </a> 10372 <a class="" href="https://calcourtsfitness.com/"> 10373 BANDAR TOGEL </a> 10374 <a class="" href="https://calcourtsfitness.com/"> 10375 TEBAK ANGKA </a> 10376 <a class="" href="https://calcourtsfitness.com/"> 10377 PIONTOGEL LINK </a> 10378 <a class="" href="https://calcourtsfitness.com/"> 10379 BANDAR TOGEL ONLINE </a> 10380 </div> 10381 <div class="mega-cats-fade" id="megaCatsFade"></div> 10382 </div> 10383 </div> 10384</nav> 10385 10386<!-- Panel Mega Menu - Outside navbar for full-screen --> 10387<div class="mm2-panel" data-mm2="panel" role="dialog" aria-label="Kategori"> 10388 <div class="mm2-view is-active" data-mm2="top"></div> 10389 <div class="mm2-view" data-mm2="detail"></div> 10390</div> 10391 10392<!-- Backdrop - Outside navbar for full-screen --> 10393<div class="mm2-backdrop" data-mm2="backdrop"></div> 10394 10395<!-- Mobile Bottom Navigation --> 10396<nav class="bottom-nav"> 10397 <div class="bottom-nav-inner"> 10398 <!-- Home --> 10399 <a href="/" class="bottom-nav-item" id="bottomHomeBtn" data-page="home"> 10400 <div class="bottom-nav-item-icon"> 10401 <!-- Outlined icon (default) --> 10402 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10403 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 10404 <polyline points="9 22 9 12 15 12 15 22"/> 10405 </svg> 10406 <!-- Filled icon (active) --> 10407 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10408 <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"/> 10409 <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"/> 10410 </svg> 10411 </div> 10412 <span class="bottom-nav-item-label">Home</span> 10413 </a> 10414 10415 <!-- Kategori --> 10416 <button class="bottom-nav-item" id="bottomCategoryBtn" data-mm2="trigger" aria-label="Kategori"> 10417 <div class="bottom-nav-item-icon"> 10418 <!-- Outlined icon (default) --> 10419 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10420 <line x1="3" y1="12" x2="21" y2="12"/> 10421 <line x1="3" y1="6" x2="21" y2="6"/> 10422 <line x1="3" y1="18" x2="21" y2="18"/> 10423 </svg> 10424 <!-- Filled icon (active) --> 10425 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10426 <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"/> 10427 </svg> 10428 </div> 10429 <span class="bottom-nav-item-label">Kategori</span> 10430 </button> 10431 10432 <!-- Keranjang --> 10433 <button class="bottom-nav-item" id="bottomCartBtn"> 10434 <div class="bottom-nav-item-icon"> 10435 <!-- Outlined icon (default) --> 10436 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10437 <circle cx="9" cy="21" r="1"/> 10438 <circle cx="20" cy="21" r="1"/> 10439 <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/> 10440 </svg> 10441 <!-- Filled icon (active) -->
10442 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10443 <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"/> 10444 </svg> 10445 <span class="bottom-nav-item-badge" id="bottomCartCount">0</span> 10446 </div> 10447 <span class="bottom-nav-item-label">Keranjang</span> 10448 </button> 10449 10450 <!-- Compare --> 10451 <button class="bottom-nav-item" id="bottomCompareBtn"> 10452 <div class="bottom-nav-item-icon"> 10453 <!-- Outlined icon (default) --> 10454 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10455 <polyline points="17 11 21 7 17 3"/> 10456 <path d="M21 7H9"/> 10457 <polyline points="7 21 3 17 7 13"/> 10458 <path d="M15 17H3"/> 10459 </svg> 10460 <!-- Filled icon (active) --> 10461 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10462 <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"/> 10463 </svg> 10464 <span class="bottom-nav-item-badge" id="bottomCompareCount" style="display:none;">0</span> 10465 </div> 10466 <span class="bottom-nav-item-label">Compare</span> 10467 </button> 10468 10469 <!-- Chat - Always show, check login in JavaScript --> 10470 <button class="bottom-nav-item" id="bottomChatBtn" data-page="chat"> 10471 <div class="bottom-nav-item-icon"> 10472 <!-- Outlined icon (default) --> 10473 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" opacity="0.85"> 10474 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 10475 </svg> 10476 <!-- Filled icon (active) --> 10477 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10478 <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/> 10479 </svg> 10480 <span class="bottom-nav-item-badge" id="bottomChatCount" style="display:none;">0</span> 10481 </div> 10482 <span class="bottom-nav-item-label">Chat</span> 10483 </button> 10484 10485 <!-- Akun --> 10486 <button class="bottom-nav-item" id="bottomLoginBtn"> 10487 <div class="bottom-nav-item-icon"> 10488 <!-- Outlined icon (default) --> 10489 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10490 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 10491 <circle cx="12" cy="7" r="4"/> 10492 </svg> 10493 <!-- Filled icon (active) --> 10494 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10495 <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"/> 10496 </svg> 10497 </div> 10498 <span class="bottom-nav-item-label">Login</span> 10499 </button> 10500 </div> 10501</nav> 10502 10503<!-- Mobile User Menu Bottom Sheet --> 10504
10505<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 10506(function(){ 10507 const btn = document.getElementById('megaMoreBtn'); 10508 const dd = document.getElementById('megaMoreDropdown'); 10509 if(!btn || !dd) return; 10510 10511 function openDD(){ 10512 dd.hidden = false; 10513 btn.setAttribute('aria-expanded','true'); 10514 document.addEventListener('click', outside, {capture:true}); 10515 document.addEventListener('keydown', esc); 10516 } 10517 function closeDD(){ 10518 if(dd.hidden) return; 10519 dd.hidden = true; 10520 btn.setAttribute('aria-expanded','false'); 10521 document.removeEventListener('click', outside, {capture:true}); 10522 document.removeEventListener('keydown', esc); 10523 } 10524 function toggle(){ dd.hidden ? openDD() : closeDD(); } 10525 function outside(e){ 10526 if(!dd.contains(e.target) && e.target !== btn){ 10527 closeDD(); 10528 } 10529 } 10530 function esc(e){ 10531 if(e.key === 'Escape'){ 10532 closeDD(); 10533 btn.focus(); 10534 } 10535 } 10536 btn.addEventListener('click', toggle); 10537})(); 10538 10539// ========== HORIZONTAL SCROLL FADE INDICATOR ========== 10540(function(){ 10541 const megaCats = document.getElementById('megaCatsScroll'); 10542 const fade = document.getElementById('megaCatsFade'); 10543 if (!megaCats || !fade) return; 10544 10545 function updateFade() { 10546 const scrollLeft = megaCats.scrollLeft; 10547 const scrollWidth = megaCats.scrollWidth; 10548 const clientWidth = megaCats.clientWidth; 10549 const maxScroll = scrollWidth - clientWidth; 10550 10551 // Hide fade when scrolled to end (within 10px tolerance) 10552 if (maxScroll - scrollLeft <= 10) { 10553 fade.style.opacity = '0'; 10554 } else { 10555 fade.style.opacity = '1'; 10556 } 10557 } 10558 10559 // Update on scroll 10560 megaCats.addEventListener('scroll', updateFade, { passive: true }); 10561 10562 // Update on resize 10563 window.addEventListener('resize', updateFade, { passive: true }); 10564 10565 // Initial check 10566 setTimeout(updateFade, 100); 10567})(); 10568 10569document.addEventListener('DOMContentLoaded', function () { 10570 var input = document.getElementById('searchInput'); 10571 if (!input) return; 10572 input.addEventListener('keydown', function (e) { 10573 if (e.key === 'Enter') { 10574 e.preventDefault(); 10575 var form = input.form; 10576 if (!form) return; 10577 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 10578 else form.submit(); 10579 } 10580 }); 10581}); 10582 10583 10584(function(){ 10585 const cartBtn = document.getElementById('cartBtn'); 10586 if (cartBtn) { 10587 cartBtn.addEventListener('click', function(e) { 10588 e.preventDefault(); 10589 10590 const checkLogin = () => { 10591 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10592 }; 10593 10594 if (checkLogin()) { 10595 window.location.href = '/keranjang'; 10596 } else { 10597 showCartLoginModal(); 10598 } 10599 }); 10600 } 10601 10602 const loginBtn = document.getElementById('loginBtn'); 10603 if (loginBtn) { 10604 loginBtn.addEventListener('click', function(e) { 10605 e.preventDefault(); 10606 window.location.href = 'https://calcourtsfitness.com/'; 10607 }); 10608 } 10609 10610 const userMenuBtn = document.getElementById('userMenuBtn'); 10611 const userDropdown = document.getElementById('userDropdown'); 10612 10613 if (userMenuBtn && userDropdown) { 10614 function openDropdown() { 10615 userDropdown.hidden = false; 10616 userMenuBtn.setAttribute('aria-expanded', 'true'); 10617 document.addEventListener('click', closeOnOutside, { capture: true }); 10618 document.addEventListener('keydown', closeOnEscape); 10619 } 10620 10621 function closeDropdown() { 10622 if (userDropdown.hidden) return; 10623 userDropdown.hidden = true; 10624 userMenuBtn.setAttribute('aria-expanded', 'false'); 10625 document.removeEventListener('click', closeOnOutside, { capture: true }); 10626 document.removeEventListener('keydown', closeOnEscape); 10627 } 10628 10629 function closeOnOutside(e) { 10630 if (!userDropdown.contains(e.target) && e.target !== userMenuBtn) { 10631 closeDropdown(); 10632 } 10633 } 10634 10635 function closeOnEscape(e) { 10636 if (e.key === 'Escape') { 10637 closeDropdown(); 10638 userMenuBtn.focus(); 10639 } 10640 } 10641 10642 userMenuBtn.addEventListener('click', function(e) { 10643 e.preventDefault(); 10644 userDropdown.hidden ? openDropdown() : closeDropdown(); 10645 });
10646 10647 // Close dropdown when clicking profile links 10648 userDropdown.querySelectorAll('[data-close-dropdown]').forEach(link => { 10649 link.addEventListener('click', function() { 10650 closeDropdown(); 10651 }); 10652 }); 10653 } 10654 10655 // Bottom nav profile button - toggle mobile bottom sheet 10656 const bottomProfileBtn = document.getElementById('bottomProfileBtn'); 10657 const mobileUserSheet = document.getElementById('mobileUserSheet'); 10658 const mobileUserSheetBackdrop = document.getElementById('mobileUserSheetBackdrop'); 10659 10660 if (bottomProfileBtn && mobileUserSheet) { 10661 function openMobileSheet() { 10662 mobileUserSheet.hidden = false; 10663 document.body.style.overflow = 'hidden'; 10664 // Trigger animation 10665 requestAnimationFrame(() => { 10666 mobileUserSheet.classList.add('active'); 10667 }); 10668 } 10669 10670 function closeMobileSheet() { 10671 mobileUserSheet.classList.remove('active'); 10672 document.body.style.overflow = ''; 10673 setTimeout(() => { 10674 mobileUserSheet.hidden = true; 10675 }, 300); 10676 } 10677 10678 bottomProfileBtn.addEventListener('click', function(e) { 10679 e.preventDefault(); 10680 openMobileSheet(); 10681 }); 10682 10683 if (mobileUserSheetBackdrop) { 10684 mobileUserSheetBackdrop.addEventListener('click', closeMobileSheet); 10685 } 10686 10687 // Close on escape key 10688 document.addEventListener('keydown', function(e) { 10689 if (e.key === 'Escape' && !mobileUserSheet.hidden) { 10690 closeMobileSheet(); 10691 } 10692 }); 10693 10694 // Close sheet when clicking profile links 10695 mobileUserSheet.querySelectorAll('[data-close-sheet]').forEach(link => { 10696 link.addEventListener('click', function() { 10697 closeMobileSheet(); 10698 }); 10699 }); 10700 } 10701 10702 // Handle Google avatar error in bottom-nav (fallback to initials) 10703 const bottomNavAvatar = document.querySelector('.bottom-nav-avatar-img'); 10704 if (bottomNavAvatar) { 10705 bottomNavAvatar.addEventListener('error', function() { 10706 const initials = this.getAttribute('data-fallback-initials') || 'U'; 10707 const initialsDiv = document.createElement('div'); 10708 initialsDiv.className = 'bottom-nav-avatar-initials'; 10709 initialsDiv.textContent = initials; 10710 this.parentNode.replaceChild(initialsDiv, this); 10711 }); 10712 } 10713 10714 // Mobile logout button 10715 const mobileLogoutBtn = document.getElementById('mobileLogoutBtn'); 10716 if (mobileLogoutBtn) { 10717 mobileLogoutBtn.addEventListener('click', async function(e) { 10718 e.preventDefault(); 10719 10720 // Check push notification before logout 10721 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10722 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10723 if (!confirmLogout) { 10724 return; // User cancelled logout 10725 } 10726 } 10727 10728 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10729 10730 try { 10731 const response = await fetch('/auth/logout', { 10732 method: 'POST', 10733 credentials: 'same-origin', 10734 headers: { 10735 'Content-Type': 'application/json', 10736 'X-CSRF-Token': getCsrfToken() 10737 }, 10738 body: JSON.stringify({ 10739 csrf_token: getCsrfToken() 10740 }) 10741 }); 10742 10743 if (response.ok) { 10744 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10745 window.guestCart.clear(); 10746 } 10747 window.location.href = '/login?logged_out=1'; 10748 } else { 10749 console.error('Logout failed'); 10750 window.location.href = '/logout'; 10751 } 10752 } catch (error) { 10753 console.error('Logout error:', error); 10754 window.location.href = '/logout'; 10755 } 10756 }); 10757 } 10758 10759 const logoutBtn = document.getElementById('logoutBtn'); 10760 if (logoutBtn) { 10761 logoutBtn.addEventListener('click', async function(e) { 10762 e.preventDefault(); 10763 10764 // Check push notification before logout 10765 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10766 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10767 if (!confirmLogout) { 10768 return; // User cancelled logout 10769 } 10770 } 10771 10772 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10773
10774 try { 10775 const response = await fetch('/auth/logout', { 10776 method: 'POST', 10777 credentials: 'same-origin', 10778 headers: { 10779 'Content-Type': 'application/json', 10780 'X-CSRF-Token': getCsrfToken() 10781 }, 10782 body: JSON.stringify({ 10783 csrf_token: getCsrfToken() 10784 }) 10785 }); 10786 10787 if (response.ok) { 10788 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10789 window.guestCart.clear(); 10790 } 10791 10792 window.location.href = '/login?logged_out=1'; 10793 } else { 10794 console.error('Logout failed'); 10795 window.location.href = '/logout'; 10796 } 10797 } catch (error) { 10798 console.error('Logout error:', error); 10799 window.location.href = '/logout'; 10800 } 10801 }); 10802 } 10803 10804 const wishlistBtn = document.getElementById('wishlistBtn'); 10805 const notifBtn = document.getElementById('notifBtn'); 10806 10807 if (wishlistBtn) { 10808 wishlistBtn.addEventListener('click', function(e) { 10809 e.preventDefault(); 10810 alert('Fitur wishlist akan segera tersedia!'); 10811 }); 10812 } 10813 10814 if (notifBtn) { 10815 notifBtn.addEventListener('click', function(e) { 10816 e.preventDefault(); 10817 alert('Fitur notifikasi akan segera tersedia!'); 10818 }); 10819 } 10820})(); 10821 10822// ========== MOBILE BOTTOM NAVIGATION ========== 10823(function initBottomNav() { 10824 // Sync cart badge with bottom nav 10825 function syncBottomCartBadge() { 10826 const mainBadge = document.getElementById('cartCount'); 10827 const bottomBadge = document.getElementById('bottomCartCount'); 10828 if (mainBadge && bottomBadge) { 10829 const count = mainBadge.textContent; 10830 bottomBadge.textContent = count; 10831 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10832 } 10833 } 10834 10835 // Sync compare badge with bottom nav 10836 function syncBottomCompareBadge() { 10837 const mainBadge = document.getElementById('compareCount'); 10838 const bottomBadge = document.getElementById('bottomCompareCount'); 10839 if (mainBadge && bottomBadge) { 10840 const count = mainBadge.textContent; 10841 bottomBadge.textContent = count; 10842 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10843 } 10844 } 10845 10846 // Initial sync 10847 syncBottomCartBadge(); 10848 syncBottomCompareBadge(); 10849 10850 // Observe badge changes 10851 const observer = new MutationObserver(function() { 10852 syncBottomCartBadge(); 10853 syncBottomCompareBadge(); 10854 }); 10855 10856 const cartBadge = document.getElementById('cartCount'); 10857 const compareBadge = document.getElementById('compareCount'); 10858 10859 if (cartBadge) observer.observe(cartBadge, { childList: true, characterData: true, subtree: true }); 10860 if (compareBadge) observer.observe(compareBadge, { childList: true, characterData: true, subtree: true }); 10861 10862 // Bottom Cart Button 10863 const bottomCartBtn = document.getElementById('bottomCartBtn'); 10864 if (bottomCartBtn) { 10865 bottomCartBtn.addEventListener('click', function(e) { 10866 e.preventDefault(); 10867 10868 const checkLogin = () => { 10869 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10870 }; 10871 10872 if (checkLogin()) { 10873 window.location.href = '/keranjang'; 10874 } else { 10875 showCartLoginModal(); 10876 } 10877 }); 10878 } 10879 10880 // Bottom Compare Button 10881 const bottomCompareBtn = document.getElementById('bottomCompareBtn'); 10882 if (bottomCompareBtn) { 10883 bottomCompareBtn.addEventListener('click', function(e) { 10884 e.preventDefault(); 10885 10886 // Close chat widget if open 10887 if (window.plazaChat && window.plazaChat.isWidgetOpen) { 10888 window.plazaChat.closeWidget(); 10889 } 10890 10891 const mainCompareBtn = document.getElementById('compareBtn'); 10892 if (mainCompareBtn) { 10893 mainCompareBtn.click(); 10894 } 10895 }); 10896 } 10897 10898 // Bottom Category Button - triggers mega menu 10899 const bottomCategoryBtn = document.getElementById('bottomCategoryBtn'); 10900 if (bottomCategoryBtn) { 10901 bottomCategoryBtn.addEventListener('click', function(e) { 10902 e.preventDefault(); 10903 // Trigger desktop mega menu 10904 const desktopTrigger = document.querySelector('.mm2-trigger:not(#bottomCategoryBtn)'); 10905 if (desktopTrigger) { 10906 desktopTrigger.click(); 10907 } 10908 });
10909 } 10910 10911 // Bottom Login Button 10912 const bottomLoginBtn = document.getElementById('bottomLoginBtn'); 10913 if (bottomLoginBtn) { 10914 bottomLoginBtn.addEventListener('click', function(e) { 10915 e.preventDefault(); 10916 window.location.href = '/login'; 10917 }); 10918 } 10919 10920 // Bottom Chat Button 10921 const bottomChatBtn = document.getElementById('bottomChatBtn'); 10922 if (bottomChatBtn) { 10923 bottomChatBtn.addEventListener('click', function(e) { 10924 e.preventDefault(); 10925 10926 // Trigger chat widget (try multiple times if needed) 10927 const triggerChat = () => { 10928 const chatToggle = document.getElementById('chat-toggle-btn'); 10929 if (chatToggle) { 10930 chatToggle.click(); 10931 return true; 10932 } 10933 return false; 10934 }; 10935 10936 // Immediate attempt 10937 if (!triggerChat()) { 10938 // Retry after 100ms if not found 10939 setTimeout(() => { 10940 if (!triggerChat()) { 10941 // Last resort: manually show popup 10942 const popup = document.getElementById('chat-popup'); 10943 if (popup) { 10944 popup.style.display = 'block'; 10945 } 10946 } 10947 }, 100); 10948 } 10949 }); 10950 } 10951 10952 // Sync chat unread badge 10953 function syncBottomChatBadge() { 10954 const chatBadge = document.getElementById('chat-unread-badge'); 10955 const bottomChatBadge = document.getElementById('bottomChatCount'); 10956 if (chatBadge && bottomChatBadge) { 10957 const count = chatBadge.textContent; 10958 bottomChatBadge.textContent = count; 10959 bottomChatBadge.style.display = chatBadge.style.display; 10960 } 10961 } 10962 10963 // Observe chat badge changes 10964 const chatBadge = document.getElementById('chat-unread-badge'); 10965 if (chatBadge) { 10966 observer.observe(chatBadge, { attributes: true, childList: true, characterData: true, subtree: true }); 10967 syncBottomChatBadge(); 10968 } 10969 10970 // Active state management based on current page 10971 function updateActiveState() { 10972 const currentPath = window.location.pathname; 10973 const bottomNavItems = document.querySelectorAll('.bottom-nav-item'); 10974 10975 bottomNavItems.forEach(item => { 10976 item.classList.remove('active'); 10977 10978 const itemId = item.id; 10979 const page = item.getAttribute('data-page'); 10980 const href = item.getAttribute('href'); 10981 10982 // Check by ID for specific pages 10983 if (itemId === 'bottomHomeBtn' && currentPath === '/') { 10984 item.classList.add('active'); 10985 } else if (itemId === 'bottomCartBtn' && (currentPath === '/keranjang' || currentPath === '/cart')) { 10986 item.classList.add('active'); 10987 } else if (itemId === 'bottomProfileBtn' && currentPath.startsWith('/profile')) { 10988 item.classList.add('active'); 10989 } else if (itemId === 'bottomCategoryBtn' && currentPath.startsWith('/kategori')) { 10990 item.classList.add('active'); 10991 } else if (itemId === 'bottomCompareBtn' && currentPath === '/compare') { 10992 item.classList.add('active'); 10993 } else if (itemId === 'bottomChatBtn' && currentPath === '/chat') { 10994 item.classList.add('active'); 10995 } 10996 // Fallback: check by data-page attribute 10997 else if (page === 'home' && currentPath === '/') { 10998 item.classList.add('active'); 10999 } else if (page === 'profile' && currentPath.startsWith('/profile')) { 11000 item.classList.add('active'); 11001 } 11002 // Fallback: check by href 11003 else if (href && currentPath === href) { 11004 item.classList.add('active'); 11005 } 11006 }); 11007 } 11008 11009 // Update active state on load 11010 updateActiveState(); 11011 11012 // Bottom nav always visible - auto-hide disabled for better UX 11013 // const bottomNav = document.querySelector('.bottom-nav'); 11014 // if (bottomNav && window.innerWidth <= 640) { 11015 // // Auto-hide feature disabled 11016 // } 11017})(); 11018// Cart Login Modal Handler (Global scope) 11019function showCartLoginModal() { 11020 const modal = document.getElementById('cartLoginModal'); 11021 if (!modal) return; 11022 11023 modal.classList.add('show'); 11024 11025 const confirmBtn = document.getElementById('cartLoginConfirm'); 11026 const registerBtn = document.getElementById('cartRegisterBtn');
11027 const cancelBtn = document.getElementById('cartLoginCancel'); 11028 11029 const handleLogin = () => { 11030 window.location.href = '/login?return=' + encodeURIComponent('/keranjang'); 11031 }; 11032 11033 const handleRegister = () => { 11034 window.location.href = '/register?return=' + encodeURIComponent('/keranjang'); 11035 }; 11036 11037 const handleCancel = () => { 11038 modal.classList.remove('show'); 11039 confirmBtn.removeEventListener('click', handleLogin); 11040 registerBtn.removeEventListener('click', handleRegister); 11041 cancelBtn.removeEventListener('click', handleCancel); 11042 }; 11043 11044 confirmBtn.removeEventListener('click', handleLogin); 11045 registerBtn.removeEventListener('click', handleRegister); 11046 cancelBtn.removeEventListener('click', handleCancel); 11047 11048 confirmBtn.addEventListener('click', handleLogin); 11049 registerBtn.addEventListener('click', handleRegister); 11050 cancelBtn.addEventListener('click', handleCancel); 11051 11052 modal.addEventListener('click', (e) => { 11053 if (e.target === modal) { 11054 handleCancel(); 11055 } 11056 }); 11057} 11058</script>
vendor: 2 bytes, line 11058
11058
11059<script data-inlined="cart-badge.js"> 11060(function(){ 11061 function selBadge(){ 11062 return document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 11063 } 11064 function setBadge(n){ 11065 const el = selBadge(); 11066 if (!el) return; 11067 const count = Math.max(0, parseInt(n||0,10) || 0); 11068 el.textContent = String(count); 11069 // Sembunyikan jika 0? Jika ingin selalu terlihat, hapus 2 baris di bawah 11070 if (count <= 0) { el.classList.add('is-empty'); } 11071 else { el.classList.remove('is-empty'); } 11072 } 11073 async function refresh(){ 11074 try{ 11075 // Primary: Use server-side endpoint (handles both logged and guest users) 11076 const res = await fetch('/api/cart/get-simple.php', { credentials:'same-origin' }); 11077 if (res.ok) { 11078 const data = await res.json(); 11079 if (data && data.ok) { 11080 setBadge(data.items_count || 0); 11081 return; 11082 } 11083 } 11084 11085 // Fallback hanya untuk guest user: check localStorage 11086 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 11087 document.body.classList.contains('logged-in') || 11088 window.userLoggedIn === true; 11089 11090 if (!isLoggedIn && window.guestCart && typeof window.guestCart.getItemsCount === 'function') { 11091 setBadge(window.guestCart.getItemsCount()); 11092 } 11093 }catch(e){ 11094 // Final fallback hanya untuk guest 11095 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 11096 if (!isLoggedIn) { 11097 try { 11098 const guestCartData = JSON.parse(localStorage.getItem('plazait_guest_cart') || '{"items":{}}'); 11099 const items = guestCartData.items || {}; 11100 const count = Object.values(items).reduce((total, item) => total + (item.qty || 0), 0); 11101 setBadge(count); 11102 } catch(e2) { 11103 setBadge(0); 11104 } 11105 } else { 11106 setBadge(0); 11107 } 11108 } 11109 } 11110 11111 // Ekspos untuk dipanggil dari skrip lain (opsional) 11112 window.CartBadge = { set: setBadge, refresh }; 11113 window.updateCartCounter = refresh; // Alias for compatibility 11114 11115 // Update saat halaman load 11116 if (document.readyState === 'loading') { 11117 document.addEventListener('DOMContentLoaded', refresh); 11118 } else { 11119 refresh(); 11120 } 11121 11122 // Dengarkan event global dari add/remove/update 11123 document.addEventListener('cart:updated', function(ev){ 11124 const c = ev && ev.detail && typeof ev.detail.items_count === 'number' 11125 ? ev.detail.items_count : null; 11126 if (c !== null) setBadge(c); 11127 else refresh(); 11128 }); 11129 11130 // Dengarkan event khusus cart remove untuk update badge 11131 document.addEventListener('cart:removed', function(ev){ 11132 // Langsung refresh untuk memastikan data terbaru 11133 refresh(); 11134 }); 11135 11136 // Jika addToCart sudah ada sebelum file ini, coba wrap agar badge ter-update. 11137 const prevAdd = window.addToCart; 11138 if (typeof prevAdd === 'function'){ 11139 window.addToCart = async function(pid, qty, vid, meta){ 11140 const res = await prevAdd(pid, qty, vid, meta); 11141 try{ 11142 const cnt = res && res.cart ? res.cart.items_count : null; 11143 if (cnt !== null) setBadge(cnt); 11144 // tetap siarkan event agar listener lain ikut sync 11145 document.dispatchEvent(new CustomEvent('cart:updated', { detail: res.cart || {} })); 11146 }catch(_){} 11147 return res; 11148 }; 11149 } 11150 11151 // Helper function to check if user is logged in 11152 function isUserLoggedIn() { 11153 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 11154 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 11155 } 11156 11157 // Make login check globally accessible 11158 window.isUserLoggedIn = isUserLoggedIn; 11159})(); 11160</script>
11160 11161 11162<!-- Product Compare Sidebar --> 11163<div class="compare-sidebar" id="compareSidebar"> 11164 <div class="compare-header"> 11165 <h4 class="compare-title"> 11166 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 11167 <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"/> 11168 </svg> 11169 Bandingkan Produk 11170 <span class="compare-count" id="compareCountSidebar">(0)</span> 11171 </h4> 11172 <button class="compare-close" id="compareClose" aria-label="Tutup"> 11173 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11174 <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"/> 11175 </svg> 11176 </button> 11177 </div> 11178 11179 <div class="compare-content"> 11180 <div class="compare-empty" id="compareEmpty"> 11181 <div class="empty-icon"> 11182 <svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" opacity="0.3"> 11183 <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"/> 11184 </svg> 11185 </div> 11186 <h5>Belum ada produk dipilih</h5> 11187 <p>Pilih produk yang ingin dibandingkan dengan klik tombol "Bandingkan Produk" di halaman produk.</p> 11188 </div> 11189 11190 <div class="compare-list" id="compareList"> 11191 <!-- Products will be populated here --> 11192 </div> 11193 </div> 11194 11195 <div class="compare-actions" id="compareActions"> 11196 <button class="btn btn-outline-danger btn-sm" id="compareClearAll"> 11197 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11198 <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"/> 11199 </svg> 11200 Hapus Semua 11201 </button> 11202 <button class="btn btn-primary btn-sm" id="compareAnalyze" disabled> 11203 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11204 <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"/> 11205 </svg> 11206 Analisis AI 11207 </button> 11208 </div> 11209 11210 <!-- AI Disclaimer in Sidebar --> 11211 <div class="compare-disclaimer"> 11212 <div class="ai-disclaimer"> 11213 <div class="disclaimer-text"> 11214 <strong>ð¡ Catatan:</strong> Analisis ini dihasilkan secara otomatis dan dapat memiliki keterbatasan. 11215 Gunakan sebagai referensi tambahan dengan tetap mempertimbangkan kebutuhan Anda. 11216 Untuk informasi lebih lanjut, silakan hubungi tim PIONTOGEL. 11217 </div> 11218 </div> 11219 </div> 11220</div><style data-inlined="pd-lightbox.css"> 11221/* PlazaIT Product Lightbox (vanilla, no CDN) â v4 */ 11222.pd-lb { 11223 position: fixed; inset: 0; z-index: 99999; 11224 background: rgba(0,0,0,.95); 11225 display: none; 11226 color: #fff; 11227 /* Transition untuk swipe-down feedback */ 11228 transition: transform .18s ease, opacity .18s ease; 11229} 11230.pd-lb.show { display: block; } 11231 11232/* Canvas di bawah (z:1), Controls di atas (z:2) */ 11233.pd-lb-canvas { 11234 position: absolute; inset: 56px 0 56px 0; 11235 display: flex; align-items: center; justify-content: center; 11236 overflow: hidden; 11237 touch-action: none; 11238 z-index: 1; 11239} 11240 11241.pd-lb-img { 11242 max-width: 96vw; max-height: 86vh; 11243 will-change: transform; 11244 user-select: none; 11245 pointer-events: none; 11246 transform-origin: 50% 50%; 11247} 11248 11249.pd-lb-close, .pd-lb-prev, .pd-lb-next, .pd-lb-counter { 11250 position: absolute; 11251 z-index: 2; 11252} 11253 11254.pd-lb-close, .pd-lb-prev, .pd-lb-next { 11255 background: rgba(17,24,39,.7); 11256 color: #fff; border: 0; cursor: pointer; 11257 width: 40px; height: 40px; border-radius: 999px; 11258 display: inline-flex; align-items: center; justify-content: center; 11259 transition: background-color .15s ease, transform .05s ease; 11260 -webkit-tap-highlight-color: transparent; 11261} 11262.pd-lb-close:hover, .pd-lb-prev:hover, .pd-lb-next:hover { background: rgba(31,41,55,.85); } 11263.pd-lb-close:active, .pd-lb-prev:active, .pd-lb-next:active { transform: translateY(.5px); } 11264 11265.pd-lb-close { top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); } 11266.pd-lb-prev { top: 50%; left: 10px; transform: translateY(-50%); } 11267.pd-lb-next { top: 50%; right: 10px; transform: translateY(-50%); } 11268 11269.pd-lb-counter { 11270 top: max(16px, env(safe-area-inset-top, 16px)); left: 50%; 11271 transform: translateX(-50%); 11272 font-weight: 700; font-size: 13px; letter-spacing: .4px;
11273 background: rgba(17,24,39,.65); 11274 padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.1); 11275} 11276 11277@media (max-width: 640px){ 11278 .pd-lb-img { max-width: 98vw; max-height: 80vh; } 11279 .pd-lb-prev, .pd-lb-next { width: 36px; height: 36px; } 11280 .pd-lb-close { width: 38px; height: 38px; } 11281} 11282</style> 11283<style>.paylater-promo{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#f0fdf4 0,#dcfce7 100%);border:1px solid #86efac;border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.4;margin:8px 0}.paylater-promo-sm{padding:6px 10px;font-size:11px;gap:6px}.paylater-promo-md{padding:8px 12px;font-size:13px;gap:8px}.paylater-promo-lg{padding:10px 16px;font-size:14px;gap:10px}.paylater-icon{font-size:1.2em;flex-shrink:0}.paylater-text{display:flex;flex-direction:column;gap:2px}.paylater-text strong{color:#15803d;font-weight:600}.paylater-text small{color:#16a34a;font-size:.85em;opacity:.9}.paylater-desktop-only{display:none}.paylater-mobile-only{display:flex}@media (min-width:641px){.paylater-desktop-only{display:flex}.paylater-mobile-only{display:none}}@media (max-width:640px){.paylater-promo{font-size:12px;padding:7px 10px}.paylater-promo-lg{font-size:13px;padding:8px 12px}}@media print{.paylater-promo{display:none!important}}.paylater-compact{display:inline-block;font-size:11px;font-weight:600;color:#059669;background:#d1fae5;padding:3px 8px;border-radius:4px;margin:6px 0 4px;line-height:1.3}@media (max-width:640px){.paylater-compact{font-size:10px;padding:2px 6px;margin:5px 0 3px}}@media print{.paylater-compact{display:none!important}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.pd-voucher-minimal{margin:10px 0;display:flex;flex-direction:column;gap:6px}.pdv-min-item{display:flex;align-items:center;gap:8px;padding:0;height:32px;transition:all .2s ease}.pdv-min-hidden{display:none}.pdv-min-disabled{opacity:.5}.pdv-badge-btn{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border:none;border-radius:8px;padding:3px;cursor:pointer;transition:all .2s ease;width:auto}.pdv-badge-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,.3)}.pdv-badge-btn:disabled{opacity:.6;cursor:not-allowed}.pdv-badge-icon{color:#fff;flex-shrink:0}.pdv-badge-text{font-size:14px;font-weight:700;color:#fff;white-space:nowrap}.pdv-badge-code{font-family:'Courier New',monospace;font-size:13px;font-weight:700;color:#fff;background:rgba(255,255,255,.2);padding:4px 8px;border-radius:4px;border:none;white-space:nowrap}.pdv-min-copy,.pdv-min-more{padding:4px 12px;font-size:12px;font-weight:600;border-radius:5px;border:1px solid #e2e8f0;background:#fff;color:#667eea;cursor:pointer;transition:all .2s ease;white-space:nowrap}.pdv-min-copy:hover:not(:disabled),.pdv-min-more:hover{background:#667eea;color:#fff;border-color:#667eea}.pdv-min-copy:disabled{opacity:.4;cursor:not-allowed}.pdv-min-copy.copied{background:#22c55e;color:#fff;border-color:#22c55e}.pdv-min-more{margin-left:auto}.modal-close{position:absolute;top:20px;right:20px;background:none;border:none;cursor:pointer;color:#64748b;padding:4px;transition:color .2s ease}.modal-close:hover{color:#1e293b}.modal-body{margin-bottom:8px}.voucher-modal-content{text-align:center}.voucher-modal-badge{display:inline-flex;flex-direction:row;align-items:center;gap:6px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border-radius:10px;padding:8px 16px;margin-bottom:12px}.vmb-value{font-size:18px;font-weight:700;color:#fff;line-height:1}.vmb-label{font-size:18px;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:.5px}.voucher-modal-code{font-family:'Courier New',monospace;font-size:14px;font-weight:700;color:#667eea;background:rgba(102,126,234,.1);padding:6px 12px;border-radius:6px;border:2px dashed rgba(102,126,234,.3);display:inline-block;margin-bottom:10px}.voucher-modal-name{font-size:14px;font-weight:600;color:#1e293b;margin-bottom:16px}.voucher-modal-details{text-align:left;background:#f8fafc;border-radius:8px;padding:12px;margin-bottom:16px}.vmd-item{display:flex;align-items:flex-start;gap:8px;padding:6px 0;font-size:13px;color:#64748b;border-bottom:1px solid #e2e8f0}.vmd-item:last-child{border-bottom:none}.vmd-item svg{color:#667eea;flex-shrink:0;margin-top:2px}.vmd-item strong{color:#1e293b}.modal-actions{padding:20px}.modal-btn-primary{background:linear-gradient(135deg,#667eea 0,#764ba2 100%);color:#fff}.modal-btn-primary:hover{opacity:.9;transform:translateY(-1px)}@media (max-width:640px){.pdv-min-item{gap:6px}.pdv-badge-btn{padding:3px;gap:6px}.pdv-badge-text{font-size:13px}.pdv-badge-code{font-size:12px;padding:3px 6px}.pdv-min-copy,.pdv-min-more{padding:3px 10px;font-size:11px}.modal-dialog{max-width:95%;margin:20px}.vmb-value{font-size:16px}.vmb-label{font-size:16px}.voucher-modal-code{font-size:13px;padding:5px 10px}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.promo-endyear-mini-banner{display:flex;align-items:center;gap:12px;padding:12px 16px;background:rgba(255,255,255,.8);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:2px solid rgba(242,140,26,.2);border-radius:12px;margin:16px 0;box-shadow:0 4px 16px rgba(0,0,0,.08);transition:all .3s ease}.promo-endyear-mini-banner:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.12);border-color:rgba(242,140,26,.4)}.pey-icon{font-size:32px;line-height:1;flex-shrink:0}.pey-content{flex:1;min-width:0}.pey-content strong{display:block;font-size:14px;font-weight:700;color:#F28C1A;margin-bottom:2px}
11283.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> 11284<style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 11285.pd-title-row{position:relative;margin-bottom:12px;padding-right:52px}.pd-title{margin:0!important}.pd-share-btn{position:absolute;top:0;right:0;width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;border:1px solid #e2e8f0;background:#fff;color:#64748b;cursor:pointer;transition:all .2s ease;-webkit-tap-highlight-color:transparent}.pd-share-btn:hover{background:#f8fafc;border-color:#cbd5e1;color:#0f172a;transform:translateY(-1px)}.pd-share-btn:active{transform:translateY(0)}.pd-share-btn svg{display:block} 11286 11287.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)}} 11288 11289.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} 11290 11291.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} 11292 11293.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)} 11294 11295.pd-share-icon{width:40px;height:40px;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;position:relative}.pd-share-icon svg{width:24px;height:24px;display:block}.pd-share-icon-whatsapp{background:linear-gradient(135deg,#25D366 0%,#128C7E 100%);color:#fff}.pd-share-icon-facebook{background:linear-gradient(135deg,#1877F2 0%,#0c63d4 100%);color:#fff}.pd-share-icon-twitter{background:linear-gradient(135deg,#1DA1F2 0%,#0d8bd9 100%);color:#fff}.pd-share-icon-telegram{background:linear-gradient(135deg,#0088cc 0%,#0077b3 100%);color:#fff}.pd-share-icon-email{background:linear-gradient(135deg,#EA4335 0%,#c5331f 100%);color:#fff}.pd-share-icon-copy{background:linear-gradient(135deg,#f1db63 0%,#e5d146 100%);color:#fff}.pd-share-icon-pdf{background:linear-gradient(135deg,#ef4444 0%,#dc2626 100%);color:#fff}.pd-share-name{font-size:12px;font-weight:600;color:#475569;text-align:center} 11296 11297.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)}
11297.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} 11298 11299@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}} 11300 11301.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} 11302 11303@media(max-width:640px){.pd-share-btn{display:none!important}} 11304 11305.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} 11306 11307.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} 11308 11309.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} 11310 11311.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} 11312 11313 11314.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} 11315 11316.sp-wa{ 11317 grid-column:3; justify-self:end; 11318 width:32px; height:32px; min-width:32px; min-height:32px; 11319 box-sizing:border-box; 11320 padding:0; line-height:0; text-decoration:none; outline:0; 11321 display:inline-flex; align-items:center; justify-content:center; 11322 border-radius:999px; border:1px solid rgba(37,211,102,.60); /* 1px untuk hasil âcrispâ */ 11323 background:rgba(37,211,102,.08); color:#25D366; 11324 transition:background-color .15s ease, border-color .15s ease, transform .05s ease; 11325 -webkit-tap-highlight-color:transparent; 11326} 11327.sp-wa:hover{background:rgba(37,211,102,.16);border-color:rgba(37,211,102,.75)} 11328.sp-wa:active{transform:translateY(.5px) scale(.98)} 11329 11330.sp-wa svg{width:18px;height:18px;display:block;vertical-align:middle} 11331.stock-popover a{text-decoration:none} 11332 11333#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}} 11334 11335.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}} 11336 11337.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}} 11338 11339.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} 11340.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)}} 11341.pwp-deals-grid{display:flex;flex-direction:column;gap:6px}.pwp-deal-card{background:#fff;border-radius:6px;overflow:hidden;border:1px solid #fbbf24;display:flex;align-items:center;padding:8px;gap:10px}.pwp-deal-card:hover{background:#fef3c7}.pwp-deal-thumb{flex-shrink:0;width:50px;height:50px;background:#f8fafc;overflow:hidden;border-radius:4px}.pwp-deal-thumb img{width:100%;height:100%;object-fit:cover}.pwp-deal-badge{background:#dc2626;color:#fff;padding:2px 5px;border-radius:3px;font-size:9px;font-weight:700;margin-left:auto}.pwp-deal-info{flex:1;display:flex;flex-direction:column;gap:4px}.pwp-deal-name{font-size:12px;font-weight:600;color:#0f172a;margin:0;line-height:1.2}.pwp-deal-prices{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.pwp-deal-price{font-size:14px;font-weight:800;color:#dc2626;white-space:nowrap}.pwp-deal-original{font-size:11px;color:#64748b;text-decoration:line-through;white-space:nowrap}.pwp-cart-note{margin-top:8px;padding:6px 10px;background:#e0f2fe;border-left:2px solid#0ea5e9;font-size:11px;color:#0c4a6e;border-radius:4px} 11342@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}} 11343 11344.preorder-badge{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;background:linear-gradient(135deg,#8b5cf6 0%,#7c3aed 100%);color:#fff;border-radius:8px;font-size:13px;font-weight:700;border:0;box-shadow:0 2px 8px rgba(139,92,246,.25)}.preorder-badge svg{width:16px;height:16px;flex-shrink:0}.preorder-info{margin:12px 0;padding:14px;background:linear-gradient(135deg,#f5f3ff 0%,#ede9fe 100%);border:1.5px solid #c4b5fd;border-radius:12px}.preorder-info-header{display:flex;align-items:center;gap:8px;margin-bottom:10px}.preorder-info-icon{width:20px;height:20px;color:#7c3aed;flex-shrink:0}.preorder-info-title{font-size:14px;font-weight:700;color:#5b21b6;margin:0}.preorder-info-items{display:flex;flex-direction:column;gap:8px}.preorder-info-row{display:flex;align-items:center;justify-content:space-between;font-size:13px}.preorder-info-label{color:#6b21a8;font-weight:600}.preorder-info-value{color:#5b21b6;font-weight:700}.preorder-info-value.highlight{color:#dc2626;font-size:14px}.preorder-info-note{margin-top:10px;padding:8px 10px;background:#fef3c7;border-left:2px solid #f59e0b;font-size:12px;color:#78350f;border-radius:6px;line-height:1.4}@media(max-width:640px){.preorder-badge{font-size:12px;padding:5px 10px}.preorder-info{margin:10px 0;padding:12px}.preorder-info-title{font-size:13px}.preorder-info-row{font-size:12px}.preorder-info-note{font-size:11px;padding:6px 8px}} 11345</style> 11346 11347<div class="pd-sticky-header" id="pd-sticky-header"><div class="pd-sticky-header-container"><div class="pd-sticky-thumb"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="PIONTOGEL" id="pd-sticky-thumb-img"></div><div class="pd-sticky-info"><h2 class="pd-sticky-title" id="pd-sticky-title">
11347Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi</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> 11348 11349<main class="product-single product-single--v3"><nav class="pd-breadcrumb" aria-label="breadcrumb"><a href="https://calcourtsfitness.com/">PIONTOGEL</a><span>-</span><a href="https://calcourtsfitness.com/">TOGEL ONLINE</a><span>-</span><a href="https://calcourtsfitness.com/">BANDAR TOGEL</a><span>-</span><a href="https://calcourtsfitness.com/">TEBAK ANGKA </a><span>-</span><a href="https://calcourtsfitness.com/">BANDAR TOGEL ONLINE</a><span>-</span><span class="current">Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi 11350 11351</span></nav> 11352 11353<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://a1.asdqwe.vip/banner/piontogel-img1" alt="TOTO TOGEL" data-slug="" loading="eager"> 11354</figure> 11355 11356<div class="pd-mobile-slider-wrapper"> 11357<div class="pd-mobile-slider" id="pd-mobile-slider" aria-label="Galeri gambar (geser)"><div class="pd-mslide"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="eager"></div><div class="pd-mslide"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></div></div><div class="pd-gallery-counter" id="pd-gallery-counter">1/6</div></div> 11358<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://a1.asdqwe.vip/banner/piontogel-img1" data-alt="TOGEL ONLINE" data-slug="" aria-label="Gambar 1"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://a1.asdqwe.vip/banner/piontogel-img1" data-alt
11358="TOGEL ONLINE" data-slug="https://a1.asdqwe.vip/banner/piontogel-img1" aria-label="Gambar 2"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://a1.asdqwe.vip/banner/piontogel-img1" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp" aria-label="Gambar 3"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://a1.asdqwe.vip/banner/piontogel-img1" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp" aria-label="Gambar 4"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://a1.asdqwe.vip/banner/piontogel-img1" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp" aria-label="Gambar 5"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://a1.asdqwe.vip/banner/piontogel-img1" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-892.webp" aria-label="Gambar 6"><img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE" loading="lazy"></button></div></section><section class="pd-info-card"><div id="pd-variant-combined" class="pd-variant-combined" hidden></div><div class="pd-title-row"> 11359 <h1 class="pd-title" id="pd-title">Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan 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> 11360<div id="pd-price-anchor"> 11361</div> 11362<div class="pd-price-qr-container"> 11363 <div class="pd-price-block" id="pd-price-block"> 11364 <div class="pd-price-row"> 11365 <span class="pd-price-current" id="pd-price-current" >Rp 10.000</span> 11366 <span class="pd-price-original" id="pd-price-original">Rp 50.000</span> 11367 <span class="pd-discount-badge" id="pd-discount-badge">-90%</span></div> 11368 <div class="pd-meta-row"> 11369 <span class="sold-inline" id="pd-sold-inline" data-role="sold-chip">Terjual Terjual 5</span> 11370 <span class="pwp-trigger-badge" id="pwp-trigger-badge" hidden> 11371 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 11372 <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> 11373 <div class="pd-meta-inline"><span class="stock-badge out" id="pd-stock-badge">PIONTOGEL</span> 11374 <span class="sku">SKU: <span id="pd-sku">BANDAR TOGEL</span></span></div> 11375<div class="paylater-promo paylater-promo-sm paylater-mobile-only"> 11376 <span class="paylater-icon">ð³</span> 11377 <span class="paylater-text"> 11378 <strong>TOGEL</strong> 11379 <small>ONLINE</small> 11380 </span> 11381</div> 11382 11383<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">
11383<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"> 11384<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-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" target="_blank" rel="noopener" aria-label="WhatsApp Asia Mega Mas"><svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><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> 11385 11386 <li class="sp-item"> 11387 <div class="sp-left"> 11388 <span class="sp-dot" style="--dot:#34d399;"></span> 11389 <span class="sp-label">Medan Fair</span> 11390 </div> 11391 <span class="sp-qty" id="sp-qty-mdf">0</span> 11392 <a class="sp-wa" 11393 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" 11394 target="_blank" rel="noopener" aria-label="WhatsApp Medan Fair"> 11395 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11396 <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"/> 11397 </svg> 11398 </a> 11399 </li> 11400 11401 <li class="sp-item"> 11402 <div class="sp-left"> 11403 <span class="sp-dot" style="--dot:#f59e0b;"></span>
11404 <span class="sp-label">Banda Aceh</span> 11405 </div> 11406 <span class="sp-qty" id="sp-qty-bda">0</span> 11407 <a class="sp-wa" 11408 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" 11409 target="_blank" rel="noopener" aria-label="WhatsApp Banda Aceh"> 11410 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11411 <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"/> 11412 </svg> 11413 </a> 11414 </li> 11415 </ul> 11416 </div> 11417 11418 </div> 11419 <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> 11420 </div> 11421 <div id="pd-variants-anchor"></div> 11422 11423 11424 11425 <div class="pd-compare-section"> 11426 <button type="button" class="pd-compare-btn" id="pd-compare-btn" data-product-id="325"> 11427 <svg class="compare-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11428 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 11429 </svg> 11430 <span class="compare-text">Bandingkan Produk</span> 11431 <span class="compare-remove" hidden> 11432 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11433 <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"/> 11434 </svg> 11435 Hapus dari Perbandingan 11436 </span> 11437 </button> 11438 <div class="pd-compare-info"> 11439 <small class="compare-note" id="compare-note">Bandingkan hingga 5 produk dengan bantuan AI</small> 11440 </div> 11441 </div> 11442 11443 <div class="pd-short-desc"><p><strong>Informasi PIONTOGEL:</strong></p><ul><li>Minimal Deposit: Rp.10.000</li><li>Jenis Permainan: Togel Online</li><li>Metode Pembayaran: Bank, QRIS, E-Wallet, Pulsa</li><li>Mata Uang: IDR (Indonesian Rupiah)</li><li>Jam Operasional: 24 Jam Nonstop</li><li>Total Rating: 888.888.333</li><li>Rating: âââââ</li><li>Daftar Buruan: <a href="https://calcourtsfitness.nichesite.org/">Klik Disini</a></li></ul></div><div class="PIONTOGEL-container-v1 mini"> 11444<div class="PIONTOGEL-container"> 11445 11446 <!-- HEADER --> 11447<div class="togel-container-v3 mini"> 11448 <div class="togel-machine-v3"> 11449 <h2 class="togel-title-v3">ð¯ TEBAK ANGKA RESMI (SIMULASI) ð¯</h2> 11450
11451 <div class="togel-controls-v3"> 11452 <label> 11453 PASARAN 11454 <select id="marketFilter"> 11455 <option value="ALL">ALL PASARAN</option> 11456 </select> 11457 </label> 11458 11459 <label> 11460 TIPE 11461 <select id="typeFilter"> 11462 <option value="4D">4D</option> 11463 <option value="3D">3D</option> 11464 <option value="2D">2D</option> 11465 </select> 11466 </label> 11467 11468 <label> 11469 MIN CONF 11470 <input id="minConf" type="number" min="50" max="99" step="1" value="70"> 11471 </label> 11472 11473 <button id="btnTogelRandom" type="button"> 11474 <span class="icon">ð²</span> GENERATE 11475 </button> 11476 </div> 11477 11478 <div class="togel-main-result-v3" id="togelResult"> 11479 <div class="togel-status-v3" id="togelStatus">â READY â</div> 11480 11481 <div class="togel-highlight-box"> 11482 <div class="togel-col-left"> 11483 <div class="k">Pasaran Rekomendasi</div> 11484 <div class="v-game" id="rMarket">-</div> 11485 11486 <div class="provider-info"> 11487 <span class="k-small">Tipe:</span> 11488 <span class="v-small" id="rType">-</span> 11489 <span class="k-small" style="margin-left:10px;">Conf:</span> 11490 <span class="v-small" id="rConf">-</span> 11491 </div> 11492 </div> 11493 11494 <div class="togel-col-right"> 11495 <div class="k-rtp">Angka Utama</div> 11496 <div class="v-rtp-huge" id="rAngka">-</div> 11497 </div> 11498 </div> 11499 11500 <div class="togel-details-v3"> 11501 <div class="togel-detail-item"> 11502 <div class="k-detail">BBFS</div> 11503 <div class="v-detail" id="rBBFS">-</div> 11504 </div> 11505 <div class="togel-detail-item"> 11506 <div class="k-detail">Angka Ikut</div> 11507 <div class="v-detail" id="rIkut">-</div> 11508 </div> 11509 <div class="togel-detail-item"> 11510 <div class="k-detail">Shio</div> 11511 <div class="v-detail" id="rShio">-</div> 11512 </div> 11513 </div> 11514 11515 <div class="togel-tip-v3" id="rTip"> 11516 Tips: Ini simulasi hiburan. Tetap bijak & disiplin. 11517 </div> 11518 </div> 11519 11520 <div class="togel-alts-v3"> 11521 <div class="togel-alts-title-v3">3 Alternatif Angka</div> 11522 <div class="togel-alt-list-v3" id="togelAltList"></div> 11523 </div> 11524 11525 <div class="togel-note-v3"> 11526 Disclaimer: Hasil di halaman ini random (simulasi), bukan acuan hasil resmi. 11527 </div> 11528 </div> 11529 </div> 11530 11531 <style> 11532 @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@600;700;800&display=swap"); 11533 11534 /* =============================== 11535 WRAPPER 11536================================ */ 11537 .togel-container-v3 { 11538 display: flex; 11539 justify-content: center; 11540 padding: 16px 10px; 11541 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 11542 } 11543 11544 /* =============================== 11545 MACHINE CORE (RED ELEGANT) 11546================================ */ 11547 .togel-machine-v3 { 11548 position: relative; 11549 width: 100%; 11550 max-width: 520px; 11551 padding: 16px; 11552 border-radius: 16px; 11553 overflow: hidden; 11554 isolation: isolate; 11555 background: radial-gradient(circle at 15% 0%, rgba(161 0 0), transparent 48%), radial-gradient(circle at 85% 100%, rgba(180, 0, 0, 0.18), transparent 55%), linear-gradient(123deg, #000000, #000000); 11556 border: 1px solid rgba(185 0 0); 11557 box-shadow: inset 0 0 18px rgba(199 0 0), 0 0 18px rgba(199 0 0); 11558 color: #dbdbdb; 11559 } 11560 11561 /* HUD SWEEP */ 11562 .togel-machine-v3::before { 11563 content: ""; 11564 position: absolute; 11565 inset: 0; 11566 background: linear-gradient(120deg, transparent 40%, rgba(255, 0, 0, .10), transparent 65%); 11567 animation: hudSweep 7s linear infinite; 11568 pointer-events: none; 11569 opacity: .8; 11570 } 11571 11572 /* =============================== 11573 TITLE 11574================================ */ 11575 .togel-title-v3 { 11576 text-align: center; 11577 margin: 0 0 12px; 11578 font-size: 15px; 11579 font-weight: 900; 11580 letter-spacing: 1px; 11581 text-shadow: 0 0 6px rgba(167 0 0 / 90%); 11582 } 11583 11584 /* =============================== 11585 CONTROLS 11586================================ */
11587 .togel-controls-v3 { 11588 display: grid; 11589 grid-template-columns: 1fr 1fr; 11590 gap: 10px; 11591 padding: 12px; 11592 border-radius: 14px; 11593 11594 background: rgba(0, 0, 0, 0.4); 11595 border: 1px solid rgba(255, 60, 60, 0.45); 11596 box-shadow: inset 0 0 14px rgba(205 0 0 / 22%); 11597 } 11598 11599 .togel-controls-v3 label { 11600 display: grid; 11601 gap: 6px; 11602 font-size: 10px; 11603 letter-spacing: .8px; 11604 text-transform: uppercase; 11605 color: #cd0000; 11606 } 11607 11608 .togel-controls-v3 select, 11609 .togel-controls-v3 input { 11610 background: rgba(0 0 0 / 92%); 11611 color: #c5c5c5; 11612 border: 1px solid rgba(205 0 0); 11613 border-radius: 10px; 11614 padding: 7px 9px; 11615 font-weight: 700; 11616 font-size: 12px; 11617 outline: none; 11618 box-shadow: inset 0 0 10px rgba(207 3 3); 11619 } 11620 11621 /* BUTTON */ 11622 #btnTogelRandom { 11623 grid-column: span 2; 11624 display: flex; 11625 align-items: center; 11626 justify-content: center; 11627 gap: 8px; 11628 11629 background: linear-gradient(145deg, #c30000, #c30000); 11630 border: none; 11631 color: #000000; 11632 font-weight: 900; 11633 letter-spacing: .8px; 11634 border-radius: 14px; 11635 cursor: pointer; 11636 padding: 10px 12px; 11637 font-size: 12px; 11638 11639 box-shadow: 0 0 16px rgba(255, 40, 40, 0.75); 11640 transition: .2s ease; 11641 } 11642 11643 #btnTogelRandom:active { 11644 transform: scale(.98); 11645 } 11646 11647 #btnTogelRandom:disabled { 11648 opacity: .75; 11649 cursor: not-allowed; 11650 } 11651 11652 /* =============================== 11653 MAIN RESULT 11654================================ */ 11655 .togel-main-result-v3 { 11656 margin-top: 12px; 11657 padding: 12px; 11658 border-radius: 14px; 11659 11660 background: rgba(30, 0, 0, 0.78); 11661 border: 1px solid rgba(223 0 0); 11662 box-shadow: inset 0 0 16px rgba(215 0 0); 11663 } 11664 11665 .togel-status-v3 { 11666 text-align: center; 11667 font-weight: 800; 11668 letter-spacing: .8px; 11669 color: #cd0000; 11670 margin-bottom: 10px; 11671 font-size: 11px; 11672 } 11673 11674 .togel-highlight-box { 11675 display: flex; 11676 justify-content: space-between; 11677 gap: 12px; 11678 padding-bottom: 10px; 11679 margin-bottom: 10px; 11680 border-bottom: 1px dashed rgba(255, 80, 80, 0.3); 11681 } 11682 11683 .k, 11684 .k-rtp { 11685 font-size: 10px; 11686 color: #c7c7c7; 11687 text-transform: uppercase; 11688 } 11689 11690 .v-game { 11691 font-size: 14px; 11692 font-weight: 900; 11693 color: #ffffff; 11694 margin-top: 6px; 11695 line-height: 1.2; 11696 } 11697 11698 .provider-info { 11699 margin-top: 8px; 11700 font-size: 11px; 11701 color: #d9d9d9; 11702 opacity: .95; 11703 } 11704 11705 .k-small { 11706 color: #cd0000; 11707 font-size: 10px; 11708 } 11709 11710 .v-small { 11711 color: #fff0f0; 11712 font-weight: 900; 11713 } 11714 11715 .v-rtp-huge { 11716 font-size: 2.4em; 11717 font-weight: 900; 11718 color: #d70000; 11719 text-shadow: 0 0 10px rgba(0 0 0); 11720 line-height: 1; 11721 letter-spacing: 1px; 11722 } 11723 11724 /* DETAILS */ 11725 .togel-details-v3 { 11726 display: grid; 11727 grid-template-columns: repeat(3, 1fr); 11728 gap: 8px; 11729 background: rgba(0, 0, 0, .30); 11730 padding: 10px; 11731 border-radius: 12px; 11732 } 11733 11734 .k-detail { 11735 font-size: 9px; 11736 color: #cd0000; 11737 text-transform: uppercase; 11738 } 11739 11740 .v-detail { 11741 font-size: 12px; 11742 font-weight: 800; 11743 color: #ffffff; 11744 margin-top: 4px; 11745 letter-spacing: .8px; 11746 } 11747 11748 .togel-tip-v3 { 11749 margin-top: 10px; 11750 font-size: 10px; 11751 color: #d9d9d9; 11752 opacity: .9; 11753 text-align: center; 11754 } 11755 11756 /* ALTS */ 11757 .togel-alts-v3 { 11758 margin-top: 12px; 11759 padding: 12px; 11760 border-radius: 14px; 11761 background: rgba(0, 0, 0, .40); 11762 border: 1px solid rgba(223 0 0); 11763 } 11764 11765 .togel-alts-title-v3 { 11766 font-weight: 900; 11767 color: #cd0000; 11768 margin-bottom: 10px; 11769 font-size: 12px; 11770 } 11771 11772 .togel-alt-list-v3 { 11773 display: grid; 11774 grid-template-columns: 1fr; 11775 gap: 8px; 11776 } 11777 11778 .togel-alt-item { 11779 background: rgba(30, 0, 0, .86); 11780 border-radius: 12px; 11781 padding: 10px; 11782 border-left: 3px solid rgba(167 0 0 / 90%); 11783 } 11784 11785 .togel-alt-item .g { 11786 font-weight: 900; 11787 color: #c5c5c5; 11788 font-size: 12px; 11789 } 11790 11791 .togel-alt-item .p { 11792 font-size: 10px; 11793 color: #cd0000; 11794 margin-top: 4px; 11795 } 11796 11797 .togel-alt-item .r { 11798 margin-top: 6px; 11799 font-weight: 900; 11800 color: #d70000; 11801 letter-spacing: .8px; 11802 font-size: 12px; 11803 } 11804 11805 .togel-note-v3 { 11806 margin-top: 10px; 11807 font-size: 10px; 11808 color: #d9d9d9; 11809 opacity: .85; 11810 text-align: center; 11811 } 11812 11813 /* MOBILE */ 11814 @media(max-width:420px) { 11815 .togel-highlight-box { 11816 flex-direction: column 11817 } 11818 11819 .togel-details-v3 { 11820 grid-template-columns: 1fr 11821 } 11822 } 11823 11824 /* ANIM */ 11825 @keyframes hudSweep { 11826 0% { 11827 transform: translateX(-110%) 11828 } 11829 11830 100% { 11831 transform: translateX(110%) 11832 } 11833 } 11834 11835 @keyframes pulse { 11836 0% { 11837 opacity: .55 11838 } 11839 11840 50% { 11841 opacity: 1 11842 } 11843 11844 100% { 11845 opacity: .55 11846 } 11847 } 11848 </style> 11849 11850
11850<script> 11851 /* =============================== 11852 DATASET TOGEL (SIMULASI) 11853 ================================ */ 11854 const TOGEL_MARKETS = [ 11855 { market: "Singapore", code: "SGP", open: "17:45", zone: "WIB" }, 11856 { market: "Hongkong", code: "HK", open: "22:45", zone: "WIB" }, 11857 { market: "Sydney", code: "SDY", open: "13:45", zone: "WIB" }, 11858 { market: "Macau", code: "MC", open: "16:00", zone: "WIB" }, 11859 { market: "Cambodia", code: "KHM", open: "10:45", zone: "WIB" }, 11860 { market: "Japan", code: "JP", open: "11:30", zone: "WIB" } 11861 ]; 11862 11863 const SHIO = ["Tikus", "Kerbau", "Macan", "Kelinci", "Naga", "Ular", "Kuda", "Kambing", "Monyet", "Ayam", "Anjing", "Babi"]; 11864 11865 const $ = (q) => document.querySelector(q); 11866 const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 11867 11868 function uniqueMarkets() { 11869 return TOGEL_MARKETS.map(m => m.market); 11870 } 11871 11872 function genDigits(len) { 11873 let s = ""; 11874 for (let i = 0; i < len; i++) s += String(randInt(0, 9)); 11875 return s; 11876 } 11877 11878 function genBBFS() { 11879 const pool = [...Array(10)].map((_, i) => i); 11880 const out = []; 11881 for (let i = 0; i < 7; i++) { 11882 const idx = randInt(0, pool.length - 1); 11883 out.push(pool.splice(idx, 1)[0]); 11884 } 11885 return out.join(""); 11886 } 11887 11888 function genIkut() { 11889 const pool = [...Array(10)].map((_, i) => i); 11890 const out = []; 11891 for (let i = 0; i < 4; i++) { 11892 const idx = randInt(0, pool.length - 1); 11893 out.push(pool.splice(idx, 1)[0]); 11894 } 11895 return out.join(" ⢠"); 11896 } 11897 11898 function simulateConfidence() { 11899 return randInt(60, 96); 11900 } 11901 11902 function applyFilters() { 11903 const m = $("#marketFilter").value; 11904 const minC = parseFloat($("#minConf").value || "0"); 11905 11906 return TOGEL_MARKETS 11907 .filter(x => (m === "ALL" || x.market === m)) 11908 .map(x => ({ ...x, conf: simulateConfidence() })) 11909 .filter(x => x.conf >= minC); 11910 } 11911 11912 function pickRandom(arr, n = 1) { 11913 const copy = [...arr]; 11914 const out = []; 11915 for (let i = 0; i < n && copy.length; i++) { 11916 const idx = randInt(0, copy.length - 1); 11917 out.push(copy.splice(idx, 1)[0]); 11918 } 11919 return out; 11920 } 11921 11922 function formatAngka(type) { 11923 if (type === "4D") return genDigits(4); 11924 if (type === "3D") return genDigits(3); 11925 return genDigits(2); 11926 } 11927 11928 function renderMain(item, type) { 11929 $("#rMarket").textContent = `${item.market} (${item.code}) ⢠Open ${item.open} ${item.zone}`; 11930 $("#rType").textContent = type; 11931 $("#rConf").textContent = `${item.conf}%`; 11932 11933 $("#rAngka").textContent = formatAngka(type); 11934 $("#rBBFS").textContent = genBBFS(); 11935 $("#rIkut").textContent = genIkut(); 11936 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 11937 $("#rTip").textContent = "Tips: Ini simulasi hiburan. Tetap bijak & disiplin."; 11938 } 11939 11940 function renderAlts(list, type) { 11941 const wrap = $("#togelAltList"); 11942 wrap.innerHTML = ""; 11943 list.forEach(x => { 11944 const el = document.createElement("div"); 11945 el.className = "togel-alt-item"; 11946 el.innerHTML = ` 11947 <div class="g">${x.market} (${x.code})</div> 11948 <div class="p">Tipe ${type} ⢠Conf <strong>${x.conf}%</strong></div> 11949 <div class="r">Angka: <strong>${formatAngka(type)}</strong></div> 11950 `; 11951 wrap.appendChild(el); 11952 }); 11953 } 11954 11955 function spinButton(btn, on) { 11956 if (on) { 11957 btn.disabled = true; 11958 btn.dataset.text = btn.innerHTML; 11959 btn.innerHTML = `<span class="icon">ð</span> PROCESS...`; 11960 } else { 11961 btn.disabled = false; 11962 btn.innerHTML = btn.dataset.text || `<span class="icon">ð²</span> GENERATE`; 11963 } 11964 } 11965 11966 /* =============================== 11967 INIT 11968 ================================ */
11969 (function init() { 11970 const sel = $("#marketFilter"); 11971 uniqueMarkets().forEach(m => { 11972 const o = document.createElement("option"); 11973 o.value = m; o.textContent = m; 11974 sel.appendChild(o); 11975 }); 11976 11977 $("#btnTogelRandom").addEventListener("click", (e) => { 11978 const btn = e.currentTarget; 11979 const type = $("#typeFilter").value; 11980 const filtered = applyFilters(); 11981 const st = $("#togelStatus"); 11982 11983 if (!filtered.length) { 11984 st.textContent = "â NO MATCH â"; 11985 st.style.color = "white"; 11986 renderAlts([], type); 11987 $("#rMarket").textContent = "-"; 11988 $("#rType").textContent = type; 11989 $("#rConf").textContent = "-"; 11990 $("#rAngka").textContent = "-"; 11991 $("#rBBFS").textContent = "-"; 11992 $("#rIkut").textContent = "-"; 11993 $("#rShio").textContent = "-"; 11994 return; 11995 } 11996 11997 spinButton(btn, true); 11998 st.textContent = "â GENERATING â"; 11999 st.style.animation = "pulse .6s linear infinite"; 12000 st.style.color = "white"; 12001 12002 const t0 = setInterval(() => { 12003 const temp = pickRandom(filtered, 1)[0]; 12004 $("#rMarket").textContent = `${temp.market} (${temp.code}) ⢠Open ${temp.open} ${temp.zone}`; 12005 $("#rType").textContent = type; 12006 $("#rConf").textContent = `${temp.conf}%`; 12007 $("#rAngka").textContent = formatAngka(type); 12008 $("#rBBFS").textContent = genBBFS(); 12009 $("#rIkut").textContent = genIkut(); 12010 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 12011 }, 85); 12012 12013 setTimeout(() => { 12014 clearInterval(t0); 12015 12016 const picks = pickRandom(filtered, 4); 12017 renderMain(picks[0], type); 12018 renderAlts(picks.slice(1), type); 12019 12020 st.textContent = "â READY â"; 12021 st.style.animation = ""; 12022 st.style.color = "gold"; 12023 spinButton(btn, false); 12024 }, 1200); 12025 }); 12026 12027 renderAlts([], "4D"); 12028 })(); 12029 </script>
12029 12030
12031<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516" integrity="sha512-8DS7rgIrAmghBFwoOTujcf6D9rXvH8xm8JQ1Ja01h9QX8EzXldiszufYa4IFfKdLUKTTrnSFXLDkUEOTrZQ8Qg==" data-cf-beacon='{"version":"2024.11.0","token":"1909e908a10849b7af66d8986368283e","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 12031
12031
12032<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"f591dd59004a48eeafbe4ab22ac1a9da","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 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":"e160a7c7e3074d01bd34414603c53b23","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 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":"7d1313fe7e8b4cc3ac480817ec77c2a1","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 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: 2 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":"57cb2e82efc64548a4a6bee0c0fb1166","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 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":"51df3b17a746455f940c0b561e8c0c0b","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
12037 12038 12039<style> 12040body{background:#ebf2fc; font-family:Poppins,sans-serif; color:white;} 12041.PIONTOGEL-container{ 12042max-width: 520px; 12043 margin: auto; 12044 padding: 15px; 12045 border-radius: 12px; 12046 background: #a10808; 12047 border: 2px solid #ca0000; 12048 box-shadow: 0 0 20px #d90000; 12049} 12050 12051.PIONTOGEL-header{ 12052 font-weight:900; 12053 text-align:center; 12054 color:#ff4c4c; 12055 text-shadow:0 0 12px #5f90d9; 12056 margin-bottom:12px; 12057} 12058 12059.PIONTOGEL-controls{ 12060 display:grid; 12061 grid-template-columns:1fr 1fr; 12062 gap:10px; 12063 margin-bottom:12px; 12064} 12065.PIONTOGEL-controls label{ 12066 display:flex; flex-direction:column; 12067 font-size:11px; color:#ff9999; 12068} 12069.PIONTOGEL-controls select, 12070.PIONTOGEL-controls input{ 12071 padding:8px; 12072 border-radius:8px; 12073 border:1px solid #ff4c4c; 12074 background:#2b0000; 12075 color:white; 12076} 12077#btnGenerate{ 12078 grid-column:span 2; 12079 padding:12px; 12080 background:#5f90d9; 12081 border:none; 12082 color:black; 12083 font-weight:900; 12084 cursor:pointer; 12085 margin-top:5px; 12086 box-shadow:0 0 15px #ff4c4c; 12087} 12088 12089.PIONTOGEL-result{ 12090 margin-top:12px; 12091 padding:12px; 12092 border-radius:10px; 12093 background:rgba(255,0,0,0.05); 12094 border:1px solid #ff4c4c; 12095} 12096
12097.PIONTOGEL-main div, .PIONTOGEL-details div{ 12098 margin:5px 0; 12099} 12100 12101#rPrediction{ 12102 font-size:24px; 12103 font-weight:900; 12104 color:#5f90d9; 12105} 12106 12107.PIONTOGEL-alts{ 12108 margin-top:10px; 12109} 12110.PIONTOGEL-alts h4{margin-bottom:6px;} 12111#PIONTOGELAltList div{ 12112 background:#2b0000; 12113 padding:6px 10px; 12114 margin:3px 0; 12115 border-radius:8px; 12116 border-left:3px solid #5f90d9; 12117} 12118 .bank-tab.active, .bank-tab:hover { 12119 background: #df0000; 12120 color: #fff; 12121 border-color: #df0000; 12122 box-shadow: 0 3px 10px -4px rgb(240 29 29 / 60%); 12123} 12124 12125.PIONTOGEL-disclaimer{ 12126 font-size:10px; 12127 margin-top:8px; 12128 text-align:center; 12129 color:#ff9999; 12130} 12131</style> 12132
12133<script> 12134const leagues = ["Premier League","La Liga","Serie A"]; 12135const tips = ["Analisis statistik tim","Performa terakhir","Cek head-to-head"]; 12136function rand(arr){return arr[Math.floor(Math.random()*arr.length)];} 12137 12138var _bgBtn=document.getElementById("btnGenerate");if(_bgBtn)_bgBtn.onclick=function(){ 12139 const type = document.getElementById("betTypeFilter").value; 12140 const league = rand(leagues); 12141 12142 let pred; 12143 if(type==="1X2") pred=rand(["1","X","2"]); 12144 else if(type==="OVER_UNDER") pred=rand(["Over","Under"]); 12145 else pred=rand(["Home -1","Away +1","Draw"]); 12146 12147 document.getElementById("rLeague").innerText=league; 12148 document.getElementById("rPrediction").innerText=pred; 12149 document.getElementById("rBetType").innerText=type; 12150 document.getElementById("rConf").innerText=Math.floor(Math.random()*40+60)+"%"; 12151 document.getElementById("rTip").innerText=rand(tips); 12152 document.getElementById("rOdds").innerText=(Math.random()*3+1).toFixed(2); 12153 12154 // alternatif 12155 const alt=document.getElementById("PIONTOGELAltList"); alt.innerHTML=""; 12156 for(let i=0;i<3;i++){let d=document.createElement("div"); d.innerText=rand(["1","X","2"]); alt.appendChild(d);} 12157 12158 document.getElementById("PIONTOGELStatus").innerText="â READY â"; 12159} 12160</script>
12160 12161 12162 12163 12164 <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> 12165 </div> 12166 </div> 12167 </section> 12168 12169 <aside class="pd-sidebar"> 12170 <div class="pd-side-card pd-buy-card"> 12171 <form class="pd-buy-form" id="pd-buy-form" data-pid="325"> 12172 <input type="hidden" name="variant_id" id="pd-variant-id" value="0"> 12173 <div> 12174 <label for="qty" class="qty-label">Qty</label> 12175 <input type="number" id="qty" name="qty" value="0" min="1" 12176 max="0" 12177 disabled> 12178 </div> 12179 <div class="pd-actions"> 12180 <button type="submit" class="btn-add-cart" id="btn-add-cart" disabled>+ Keranjang</button> 12181 <button type="button" class="btn-buy" id="btn-buy" data-id="278426" disabled >Beli</button> 12182 <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> 12183 </div> 12184 </form> 12185 <div class="paylater-promo paylater-promo-md paylater-desktop-only"> 12186 <span class="paylater-icon">ð³</span> 12187 <span class="paylater-text"> 12188 <strong>Autorized PIONTOGEL</strong> 12189 <small>Situs Resmi PIONTOGEL </small> 12190 </span> 12191</div> </div> 12192 12193 12194 <div class="pd-side-card qoala-banner-card"> 12195 <div class="qoala-banner"> 12196 <div class="qoala-banner-content"> 12197 <div class="qoala-icon"> 12198 <center><img src="https://a1.asdqwe.vip/assets/icon-piontogel.png" alt="PIONTOGEL" class="qoala-shield-img"></center> 12199 </div> 12200 <div class="qoala-text"> 12201 <div class="qoala-title">Login PIONTOGEL</div> 12202 <div class="qoala-subtitle">Verified</div> 12203 </div> 12204 </div> 12205 <a href="https://calcourtsfitness.nichesite.org/" class="qoala-cta" 12206 data-ga-event="qoala_banner_click" 12207 data-product-id="325" 12208 data-product-name="PIONTOGEL" 12209 data-category-path="Komputer & Laptop/Laptop/Gaming" target="_blank">Info</a> 12210 </div> 12211<style> 12212.qoala-banner-card{padding:0;overflow:hidden} 12213 12214.qoala-banner{ 12215 background:linear-gradient(135deg, #cd0000 0%, #c90000 100%); 12216 padding:12px 16px; 12217 display:flex; 12218 align-items:center; 12219 justify-content:space-between; 12220 color:white; 12221 text-decoration:none; 12222 min-height:60px; 12223} 12224 12225.qoala-banner-content{ 12226 display:flex; 12227 align-items:center; 12228 gap:12px; 12229} 12230 12231.qoala-icon{ 12232 display:flex; 12233 align-items:center; 12234 justify-content:center; 12235} 12236 12237.qoala-shield-img{ 12238 width:36px; 12239 height:36px; 12240 object-fit:contain; 12241 opacity:0.9; 12242} 12243 12244.qoala-title{ 12245 font-weight:600; 12246 font-size:14px; 12247 line-height:1.2; 12248} 12249 12250.qoala-subtitle{ 12251 font-size:12px; 12252 opacity:0.9; 12253 line-height:1.2; 12254} 12255 12256.qoala-cta{ 12257 background:rgba(255,255,255,0.15); 12258 color:white; 12259 padding:6px 12px; 12260 border-radius:6px; 12261 font-size:12px; 12262 font-weight:600; 12263 text-decoration:none; 12264 transition:background 0.2s ease; 12265 border:1px solid rgba(255,80,80,0.4); 12266} 12267 12268.qoala-cta:hover{ 12269 background:rgba(255,60,60,0.25); 12270 color:white; 12271} 12272 12273@media(max-width:768px){ 12274 .qoala-banner{padding:10px 12px} 12275 .qoala-title{font-size:17px} 12276 .qoala-subtitle{font-size:11px} 12277} 12278</style> 12279 </div> 12280 12281 <div class="pd-side-card pd-installment-card2" 12282 id="inst-card" 12283 data-base-price="20789000" 12284 data-zero-smart="0"> 12285 <div class="inst-head"> 12286 <h3>Promo & Fitur PIONTOGEL</h3> 12287 </div> 12288 12289 <div class="inst-bank-tabs" role="tablist" aria-label="Bank"> 12290 <button type="button" class="bank-tab active" data-bank="Lucky Draw" aria-selected="true">Lucky Draw</button> 12291 <button type="button" class="bank-tab" data-bank="Lomba Panjat Tur
12291nover" aria-selected="false">Lomba Panjat Turnover</button> 12292 <button type="button" class="bank-tab" data-bank="Prediksi Jitu" aria-selected="false">Prediksi Jitu</button> 12293 </div> 12294 12295 <ul class="inst-month-list" id="inst-month-list" aria-label="Daftar cicilan per bulan"></ul> 12296 <div class="inst-note" id="inst-zero-note" style="display:none;">0% Cicilan khusus bank terpilih*</div> 12297 </div> 12298 12299 <!-- MECHA BLUE FINAL BOSS REVIEW SLIDER --> 12300<div class="tnt-review-mecha"> 12301 12302<div class="tnt-header"> 12303<h2>REVIEW MEMBER PIONTOGEL</h2> 12304 12305</div> 12306 12307<div class="tnt-slider" id="tntSlider"> 12308 12309<div class="tnt-slide active"> 12310 <div class="tnt-name">Rian Pratama â Surabaya</div> 12311 <div class="tnt-comment">"Login VIP-nya cepat banget dan semua informasi langsung terlihat jelas. Live draw selalu update, jadi memantau result terbaru terasa makin praktis."</div> 12312</div> 12313 12314<div class="tnt-slide"> 12315 <div class="tnt-name">Vinka Aurelia â Semarang</div> 12316 <div class="tnt-comment">"Pasarannya lengkap dan riwayat keluaran tersusun rapi. Saya jadi lebih mudah membandingkan result sekaligus melihat pola angka yang sering muncul."</div> 12317</div> 12318 12319<div class="tnt-slide"> 12320 <div class="tnt-name">Violet Evergarde â Makassar</div> 12321 <div class="tnt-comment">"Menurut saya konsepnya cukup beda. Informasi yang ditampilkan nggak terasa terlalu penuh dan masih nyaman buat dijelajahi."</div> 12322</div> 12323 12324 12325 12326</div> 12327</div> 12328 12329</aside> 12330</div> 12331 12332<style> 12333.tnt-review-mecha{ 12334 background: radial-gradient(circle at center, #da0000, #350000); 12335 border: 2px solid #aa0000; 12336 border-radius: 14px; 12337 padding: 20px; 12338 color: #dbdbdb; 12339 font-family: 'Orbitron', sans-serif; 12340 position: relative; 12341 overflow: hidden; 12342 box-shadow: 0 0 25px #990000, inset 0 0 20px rgba(255, 0, 0, 0.2); 12343} 12344 12345/* scanline */ 12346.tnt-review-mecha::before{ 12347 content:""; 12348 position:absolute; 12349 inset:0; 12350 background: repeating-linear-gradient( 12351 to bottom, 12352 rgba(255,0,0,0.08), 12353 rgba(255,0,0,0.08) 1px, 12354 transparent 2px, 12355 transparent 4px 12356 ); 12357 pointer-events:none; 12358}.bank-tab.active, .bank-tab:hover 12359 12360 12361.tnt-header h2{ 12362 font-size: 18px; 12363 color:#000000; 12364 text-shadow:0 0 10px #d71414, 0 0 20px #d10000; 12365 margin-bottom:5px; 12366} 12367 12368.tnt-header p{ 12369 font-size:12px; 12370 opacity:0.85; 12371 margin-bottom:15px; 12372} 12373 12374.tnt-slider{ 12375 position:relative; 12376 height:90px; 12377} 12378 12379.tnt-slide{ 12380 position:absolute; 12381 width:100%; 12382 opacity:0; 12383 transform:translateX(40px); 12384 transition:all .6s ease; 12385} 12386 .tenor-badge { 12387 background: #df0000; 12388 color: #fff; 12389 font-weight: 700; 12390 min-width: 42px; 12391 text-align: center; 12392 padding: .38rem .4rem; 12393 font-size: .8rem; 12394 border-radius: 10px; 12395 box-shadow: 0 3px 10px -4px rgb(211 3 3); 12396} 12397 12398.tnt-slide.active{ 12399 opacity:1; 12400 transform:translateX(0); 12401} 12402 12403.tnt-name{ 12404font-size: 14px; 12405 color: #000000; 12406 margin-bottom: 5px; 12407 text-shadow: 0 0 6px #290000; 12408} 12409 12410.tnt-comment{ 12411 font-size:13px; 12412 line-height:1.5; 12413} 12414 12415/* glowing border animation */ 12416@keyframes pulseGlow{ 12417 0%{box-shadow:0 0 10px #d71414;} 12418 50%{box-shadow:0 0 25px #d71414;} 12419 100%{box-shadow:0 0 10px #d71414;} 12420} 12421 12422.tnt-review-mecha{ 12423 animation:pulseGlow 2.5s infinite; 12424} 12425</style> 12426
12427<script> 12428let slides = document.querySelectorAll('.tnt-slide'); 12429let index = 0; 12430 12431setInterval(()=>{ 12432 slides[index].classList.remove('active'); 12433 index = (index + 1) % slides.length; 12434 slides[index].classList.add('active'); 12435}, 3000); 12436</script>
12436 12437 12438 <div class="artikel-glow-SM"> 12439 12440 <div class="artikel-shine"></div> 12441 12442 <h1>Menghadirkan PIONTOGEL - Situs Alami Mendapatkan Cuan Dengan Permainan Resmi</h1> 12443 <p style="text-align: justify;">PIONTOGEL membangun identitas sebagai platform yang mencoba menghadirkan pengalaman dalam satu ruang dengan karakter yang lebih terarah.</p> 12444 12445 <p style="text-align: justify;">PIONTOGEL mengambil pendekatan berbeda dengan menempatkan setiap bagian berdasarkan fungsi dan kebutuhan. Gagasan tersebut membuat ruang digital tidak sekadar dipenuhi berbagai elemen, melainkan mempunyai alur yang dapat dipahami sejak kunjungan pertama.</p> 12446</div> 12447 12448<style> 12449.artikel-glow-SM{ 12450 max-width: 820px; 12451 margin: 40px auto; 12452 padding: 36px; 12453 box-sizing: border-box; 12454 position: relative; 12455 overflow: hidden; 12456 border-radius: 30px; 12457 font-family: Arial, sans-serif; 12458 color: #ffffff; 12459 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); 12460 border: 2px solid rgba(255, 255, 255, .55); 12461 box-shadow: 0 0 25px rgb(207 3 3), 0 0 55px rgb(225 0 0), 0 20px 45px rgba(0, 0, 0, .25), inset 0 0 35px rgba(145 0 0); 12462} 12463 12464.artikel-glow-SM:before{ 12465 content:""; 12466 position:absolute; 12467 inset:10px; 12468 border-radius:24px; 12469 border:1px solid rgba(255,255,255,.32); 12470 pointer-events:none; 12471} 12472 12473.artikel-shine{ 12474 position:absolute; 12475 top:-120px; 12476 left:-180px; 12477 width:280px; 12478 height:650px; 12479 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12480 transform:rotate(25deg); 12481 animation:artikelShine 4.5s infinite; 12482 pointer-events:none; 12483} 12484 12485@keyframes artikelShine{ 12486 0%{left:-220px;} 12487 55%{left:110%;} 12488 100%{left:110%;} 12489} 12490 12491.artikel-glow-SM h1, 12492.artikel-glow-SM h2{ 12493 position:relative; 12494 color:#000000; 12495 text-align:center; 12496 line-height:1.35; 12497 text-shadow: 12498 0 0 8px rgba(179 0 0 / 95%), 12499 0 0 18px rgba(197 0 0), 12500 0 2px 8px rgba(149 0 0); 12501} 12502 12503.artikel-glow-SM h1{ 12504 font-size:31px; 12505 margin:0 0 20px; 12506} 12507 12508.artikel-glow-SM h2{ 12509 font-size:24px; 12510 margin:28px 0 14px; 12511} 12512 12513.artikel-glow-SM p{ 12514 position:relative; 12515 margin:0 0 16px; 12516 color:#f8ffff; 12517 font-size:15px; 12518 line-height:1.85; 12519 text-align:justify; 12520 text-shadow:0 1px 4px rgba(0,0,0,.28); 12521} 12522 12523.artikel-glow-SM a{ 12524 color:#000000; 12525 font-weight:800; 12526 text-shadow:0 0 8px rgba(153 0 0); 12527} 12528 12529.artikel-glow-SM img{ 12530 position:relative; 12531 display:block; 12532 max-width:100%; 12533 margin:16px auto 24px; 12534 border-radius:16px; 12535 border:1px solid rgba(255,255,255,.55); 12536 box-shadow: 12537 0 0 18px rgba(255,255,255,.55), 12538 0 12px 28px rgba(0,0,0,.28); 12539} 12540 12541.artikel-highlight{ 12542 position:relative; 12543 margin:24px 0; 12544 padding:22px; 12545 border-radius:22px;
12546 background:rgba(255,255,255,.20); 12547 border:1px solid rgba(255,255,255,.45); 12548 box-shadow: 12549 inset 0 1px 0 rgba(255,255,255,.65), 12550 inset 0 0 22px rgba(255,255,255,.14), 12551 0 10px 25px rgba(0,0,0,.18); 12552 backdrop-filter:blur(8px); 12553} 12554 12555.artikel-highlight:before{ 12556 content:""; 12557 position:absolute; 12558 left:0; 12559 right:0; 12560 top:0; 12561 height:45%; 12562 border-radius:22px 22px 0 0; 12563 background:linear-gradient(rgba(255,255,255,.35),rgba(255,255,255,.04)); 12564 pointer-events:none; 12565} 12566 12567.artikel-glow-SM ul{ 12568 position:relative; 12569 margin:15px 0 0 20px; 12570 padding:0; 12571} 12572 12573.artikel-glow-SM li{ 12574 margin-bottom:11px; 12575 padding:10px 12px; 12576 border-radius:12px; 12577 background:rgba(255,255,255,.16); 12578 color:#ffffff; 12579 line-height:1.6; 12580 box-shadow:inset 0 0 10px rgba(255,255,255,.08); 12581} 12582 12583@media(max-width:600px){ 12584 .artikel-glow-SM{ 12585 margin:25px 12px; 12586 padding:25px; 12587 border-radius:24px; 12588 } 12589 12590 .artikel-glow-SM h1{ 12591 font-size:24px; 12592 } 12593 12594 .artikel-glow-SM h2{ 12595 font-size:21px; 12596 } 12597} 12598</style> 12599 12600<div class="SM-faq-rainbow"> 12601 <div class="SM-shine"></div> 12602 12603 <div class="SM-faq-top"> 12604 <span>â PREMIUM FAQ â </span> 12605 <h2>PERTANYAAN POPULER PIONTOGEL</h2> 12606 12607 </div> 12608 12609 <div class="SM-rainbow-item"> 12610 <button type="button">Apa yang menjadi ciri khas PIONTOGEL?</button> 12611 <div class="SM-rainbow-answer"> 12612 <p>PIONTOGEL mengusung konsep ruang digital yang memadukan keberagaman permainan, penyajian informasi, dan pengalaman penggunaan dalam satu platform.</p> 12613 </div> 12614 </div> 12615 12616 <div class="SM-rainbow-item"> 12617 <button type="button">Mengapa keberagaman permainan menjadi bagian penting?</button> 12618 <div class="SM-rainbow-answer"> 12619 <p>Keberagaman memberikan keleluasaan bagi pengguna untuk mengenali berbagai pilihan yang tersedia sesuai dengan ketertarikannya masing-masing.</p> 12620 </div> 12621 </div> 12622 12623 <div class="SM-rainbow-item"> 12624 <button type="button">Apa yang dimaksud dengan perspektif berbeda pada PIONTOGEL?</button> 12625 <div class="SM-rainbow-answer"> 12626 <p>Perspektif tersebut merujuk pada cara platform menyusun informasi dan menghadirkan pilihan hiburan secara lebih terarah, bukan sekadar menampilkan banyak elemen sekaligus.</p> 12627 </div> 12628 </div> 12629 12630 <div class="SM-rainbow-item"> 12631 <button type="button">Bagaimana cara mengenali permainan yang tersedia?</button> 12632 <div class="SM-rainbow-answer"> 12633 <p>Pengguna dapat melihat kategori dan informasi yang disediakan pada platform, kemudian memilih bagian yang ingin dieksplorasi berdasarkan keterangannya.</p> 12634 </div> 12635 </div> 12636 <div class="SM-rainbow-item"> 12637 <button type="button">Apakah PIONTOGEL cocok untuk pengguna baru?</button> 12638 <div class="SM-rainbow-answer"> 12639 <p>Platform dengan penyajian informasi yang terstruktur dapat membantu pengguna baru mengenali berbagai bagian secara bertahap tanpa harus memahami seluruh fitur sekaligus.</p> 12640 </div> 12641 </div> 12642</div> 12643 12644<style> 12645.SM-faq-rainbow{ 12646 max-width: 760px; 12647 margin: 40px auto; 12648 padding: 30px; 12649 box-sizing: border-box; 12650 position: relative; 12651 overflow: hidden; 12652 border-radius: 32px; 12653 font-family: Arial, sans-serif; 12654 background: radial-gradient(circle at 15% 10%, rgba(177 2 2), transparent 18%), radial-gradient(circle at 85% 15%, rgb(207 3 3), transparent 25%), radial-gradient(circle at 20% 90%, rgb(207 3 3), transparent 25%), linear-gradient(135deg, #c70000, #8e0b0b, #960e0e, #c30000); 12655 border: 2px solid rgba(177 2 2); 12656 box-shadow: 0 0 25px rgb(207 3 3), 0 0 55px rgba(223 0 0), inset 0 0 35px rgba(145 0 0); 12657} 12658 12659.SM-shine{ 12660 position:absolute; 12661 top:-90px; 12662 left:-120px; 12663 width:260px; 12664 height:520px; 12665 background:linear-gradient(120deg,transparent,rgba(255,255,255,.55),transparent); 12666 transform:rotate(25deg); 12667 animation:SMShine 4s infinite; 12668 pointer-events:none; 12669} 12670 12671@keyframes SMShine{ 12672 0%{left:-180px;} 12673 55%{left:110%;} 12674 100%{left:110%;} 12675} 12676 12677.SM-faq-top{ 12678 position:relative; 12679 text-align:center; 12680 margin-bottom:24px; 12681} 12682 12683.SM-faq-top span{ 12684 display:inline-block; 12685 padding:8px 22px; 12686 border-radius:999px; 12687 background:rgba(185 0 0 / 0%); 12688 color:#dbdbdb; 12689 font-size:12px; 12690 font-weight:900; 12691 letter-spacing:2px; 12692 border:1px solid rgba(215 215 215 / 62%); 12693 box-shadow:inset 0 0 14px rgba(255,255,255,.25); 12694} 12695 12696.SM-faq-top h2{ 12697 margin:16px 0 8px; 12698 color:#000000; 12699 font-size:34px; 12700 line-height:1.15; 12701 text-shadow: 12702 0 0 10px rgba(177 0 0), 12703 0 0 22px rgba(173 0 0); 12704} 12705 12706.SM-faq-top p{ 12707 margin:0; 12708 color:#f3ffff; 12709 font-size:14px; 12710} 12711 12712.SM-rainbow-item{ 12713 position:relative; 12714 overflow:hidden; 12715 margin-bottom:14px; 12716 border-radius:20px; 12717 background:rgba(255,255,255,.20); 12718 border:1px solid rgba(255,255,255,.48); 12719 box-shadow: 12720 inset 0 1px 0 rgba(255,255,255,.6), 12721 0 10px 25px rgba(0,0,0,.18); 12722 backdrop-filter:blur(10px); 12723} 12724 12725.SM-rainbow-item:before{ 12726 content:""; 12727 position:absolute; 12728 top:0; 12729 left:0; 12730 right:0; 12731 height:45%; 12732 background:linear-gradient(rgba(255,255,255,.32),rgba(255,255,255,.04)); 12733 pointer-events:none; 12734} 12735 12736.SM-rainbow-item button{ 12737 width:100%; 12738 padding:18px 58px 18px 20px; 12739 border:0; 12740 background: #af0000;
12741 color:#dbdbdb; 12742 text-align:left; 12743 cursor:pointer; 12744 font-size:15px; 12745 font-weight:900; 12746 position:relative; 12747 z-index:2; 12748 text-shadow:0 2px 6px rgba(0,0,0,.35); 12749} 12750 12751.SM-rainbow-item button:after{ 12752 content:"+"; 12753 position:absolute; 12754 right:18px; 12755 top:50%; 12756 transform:translateY(-50%); 12757 width:34px; 12758 height:34px; 12759 border-radius:50%; 12760 background:linear-gradient(135deg,#b10000, #9b0000); 12761 color:#d1d1d1; 12762 text-align:center; 12763 line-height:34px; 12764 font-size:24px; 12765 font-weight:900; 12766 box-shadow: 12767 0 0 14px rgba(255,255,255,.8), 12768 inset 0 -4px 8px rgba(0,0,0,.18); 12769} 12770 12771.SM-rainbow-item.active button:after{ 12772 content:"â"; 12773 background:linear-gradient(135deg,#ff0000, #5b0000); 12774} 12775 12776.SM-rainbow-answer{ 12777 max-height:0; 12778 overflow:hidden; 12779 transition:max-height .35s ease; 12780 position:relative; 12781 z-index:2; 12782} 12783 12784.SM-rainbow-item.active .SM-rainbow-answer{ 12785 max-height:170px; 12786} 12787 12788.SM-rainbow-answer p{ 12789 margin:0; 12790 padding:0 20px 18px; 12791 color:#dbdbdb; 12792 font-size:14px; 12793 line-height:1.7; 12794 text-shadow:0 2px 5px rgba(0 0 0); 12795} 12796 12797@media(max-width:600px){ 12798 .SM-faq-rainbow{ 12799 margin:25px 12px; 12800 padding:22px; 12801 border-radius:24px; 12802 } 12803 12804 .SM-faq-top h2{ 12805 font-size:26px; 12806 } 12807} 12808</style> 12809
12810<script> 12811document.querySelectorAll(".SM-rainbow-item button").forEach(function(btn){ 12812 btn.addEventListener("click", function(){ 12813 this.parentElement.classList.toggle("active"); 12814 }); 12815}); 12816</script>
12816 12817<style> 12818@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap'); 12819 12820.cyber-mecha { 12821 max-width: 800px; 12822 margin: 40px auto; 12823 12824 /* deep luxury red */ 12825 background: radial-gradient(circle at top, #14b1d7, #14b1d7 70%); 12826 12827 border-radius: 16px; 12828 border: 1px solid rgba(255, 90, 90, 0.35); 12829 12830 box-shadow: 12831 0 0 25px rgba(140, 30, 30, 0.35), 12832 inset 0 0 20px rgba(255, 60, 60, 0.10); 12833 12834 padding: 25px; 12835 font-family: 'Orbitron', sans-serif; 12836 color: #f7e6e6; 12837 position: relative; 12838 overflow: hidden; 12839} 12840 12841/* scanline overlay */ 12842.cyber-mecha::before { 12843 content: ""; 12844 position: absolute; 12845 inset: 0; 12846 background: repeating-linear-gradient( 12847 to bottom, 12848 rgba(255,90,90,0.03), 12849 rgba(255,90,90,0.03) 1px, 12850 transparent 1px, 12851 transparent 3px 12852 ); 12853 pointer-events: none; 12854} 12855 12856.pd-full-desc-title { 12857 text-align: center; 12858 font-size: 22px; 12859 12860 /* elegant muted red */ 12861 color: #e08b8b; 12862 12863 letter-spacing: 2px; 12864 text-shadow: 0 0 10px rgba(224, 139, 139, 0.4); 12865 margin-bottom: 20px; 12866} 12867 12868.mecha-header h3 { 12869 text-align: center; 12870 font-size: 18px; 12871 color: #ffb3b3; 12872 text-shadow: 0 0 8px rgba(255, 90, 90, 0.5); 12873 margin-bottom: 15px; 12874} 12875 12876.mecha-image img { 12877 width: 100%; 12878 border-radius: 10px; 12879 border: 1px solid rgba(255, 90, 90, 0.4); 12880 box-shadow: 0 0 20px rgba(255, 60, 60, 0.25); 12881 margin-bottom: 20px; 12882} 12883 12884.mecha-content p { 12885 font-size: 14px; 12886 line-height: 1.8; 12887 margin-bottom: 14px; 12888 color: #f3dede; 12889} 12890 12891.glow { 12892 color: #e08b8b; 12893 text-shadow: 0 0 6px rgba(224, 139, 139, 0.5); 12894 font-weight: 600; 12895} 12896 12897.glow-strong { 12898 color: #ff6b6b; 12899 text-shadow: 12900 0 0 10px rgba(255, 90, 90, 0.6), 12901 0 0 20px rgba(255, 60, 60, 0.4); 12902 font-weight: 700; 12903} 12904 12905.final-line { 12906 margin-top: 18px; 12907 padding: 10px; 12908 border: 1px solid rgba(255, 90, 90, 0.35); 12909 background: rgba(60, 0, 0, 0.25); 12910 border-radius: 8px; 12911 text-align: center; 12912 color: #ffd0d0; 12913} 12914</style> 12915 12916 12917 12918 <section class="pd-related-full"> 12919 <h2 class="pd-related-title">Produk Terkait</h2> 12920 <div class="pd-related-grid pd-related-grid--full"> 12921 <a class="pd-related-card" href="https://calcourtsfitness.com/"> 12922 <div class="thumb"> 12923 <img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="PIONTOGEL"> 12924 <span class="mini-disc">-2%</span> </div> 12925 <div class="info"> 12926 <h3>PIONTOGEL</h3> 12927 <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"> 12928 <span class="final">Rp 11.500.000</span> 12929 <span class="ori">Rp 12.799.000</span> 12930 </span> 12931 <span class="sold">Terjual 200+</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12932 </div> 12933 </a> 12934 <a class="pd-related-card" href="https://calcourtsfitness.com/"> 12935 <div class="thumb"> 12936 <img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOTO TOGEL"> 12937 <span class="mini-disc">-1%</span> </div> 12938 <div class="info"> 12939 <h3>TOGEL</h3> 12940 <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"> 12941 <span class="final">Rp 22.887.000</span> 12942 <span class="ori">Rp 21.999.000</span> 12943 </span> 12944 <span class="sold">Terjual 20+</span> <span class="stock-dot out" title="Habis"></span> 12945 </div> 12946 </a> 12947 <a class="pd-related-card" href="https://calcourtsfitness.com/"> 12948 <div class="thumb"> 12949 <img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE">
12950 <span class="mini-disc">-1%</span> </div> 12951 <div class="info"> 12952 <h3>TEBAK ANGKA</h3> 12953 <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"> 12954 <span class="final">Rp 23.600.000</span> 12955 <span class="ori">Rp 21.399.000</span> 12956 </span> 12957 <span class="sold">Terjual 6</span> <span class="stock-dot ready" title="Ready"></span> 12958 </div> 12959 </a> 12960 <a class="pd-related-card" href="https://calcourtsfitness.com/"> 12961 <div class="thumb"> 12962 <img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL 4D"> 12963 <span class="mini-disc">-1%</span> </div> 12964 <div class="info"> 12965 <h3>BANDAR TOGEL</h3> 12966 <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"> 12967 <span class="final">Rp 18.800.000</span> 12968 <span class="ori">Rp 19.799.000</span> 12969 </span> 12970 <span class="sold">Terjual 5</span> <span class="stock-dot ready" title="Ready"></span> 12971 </div> 12972 </a> 12973 <a class="pd-related-card" href="https://calcourtsfitness.com/"> 12974 <div class="thumb"> 12975 <img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOTO TOGEL 4D"> 12976 <span class="mini-disc">-1%</span> </div> 12977 <div class="info"> 12978 <h3>TOGEL RESMI</h3> 12979 <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"> 12980 <span class="final">Rp 28.600.080</span> 12981 <span class="ori">Rp 27.999.000</span> 12982 </span> 12983 <span class="sold">Terjual 6</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12984 </div> 12985 </a> 12986 <a class="pd-related-card" href="https://calcourtsfitness.com/"> 12987 <div class="thumb"> 12988 <img src="https://a1.asdqwe.vip/banner/piontogel-img1" alt="TOGEL ONLINE"> 12989 <span class="mini-disc">-1%</span> </div> 12990 <div class="info"> 12991 <h3>BANDAR TOGEL ONLINE</h3> 12992 <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"> 12993 <span class="final">Rp 17.888.000</span> 12994 <span class="ori">Rp 18.499.000</span> 12995 </span> 12996 <span class="stock-dot low" title="Stok Terbatas"></span> 12997 </div> 12998 </a> 12999 13000 </div> 13001 </section> 13002 13003 <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
13003-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> 13004 13005 <button type="button" 13006 class="pd-scroll-top" 13007 id="pd-scroll-top" 13008 aria-label="Kembali ke atas" 13009 title="Kembali ke atas"> 13010 <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"> 13011 <path fill="currentColor" d="M7.41 14.59 12 10l4.59 4.59L18 13.17l-6-6-6 6z"/> 13012 </svg> 13013 </button> 13014</main> 13015 13016<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://a1.asdqwe.vip/banner/piontogel-img1" alt="PIONTOGEL" class="pd-share-preview-img"><div class="pd-share-preview-info"><h3 class="pd-share-preview-name">PIONTOGEL</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">
13016<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://calcourtsfitness.com/" 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> 13017 13018<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;
13018left: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> 13019<style> 13020.toast .msg { word-break: break-word; overflow-wrap: anywhere; } 13021 13022.toast-root { bottom: max(12px, env(safe-area-inset-bottom, 12px)); } 13023 13024@media (max-width: 540px) { 13025 .toast-root { 13026 left: 0; right: 0; transform: none; 13027 padding: 0 12px; 13028 bottom: calc(max(12px, env(safe-area-inset-bottom, 12px)) + var(--fab-offset, 72px)); 13029 max-width: none; 13030 } 13031 .toast { 13032 grid-template-columns: 1fr; /* 1 kolom */ 13033 position: relative; 13034 padding: 14px 14px 12px 14px; 13035 border-radius: 16px; 13036 } 13037 .toast .ico { 13038 position: absolute; left: 14px; top: 14px; 13039 margin: 0; transform: translateY(1px); 13040 } 13041 .toast .cnt { padding-left: 34px; } 13042 .toast .ttl { font-size: 15px; margin-bottom: 6px; } 13043 .toast .msg { font-size: 13px; opacity: .96; } 13044 .toast .actions { 13045 margin-top: 12px; gap: 10px; width: 100%; 13046 display: grid; grid-template-columns: 1fr 1fr; 13047 } 13048 .toast .btn { width: 100%; min-width: 0; padding: 10px 0; } 13049 .toast .close { position: absolute; right: 6px; top: 6px; } 13050} 13051</style> 13052
13053<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13054(function adjustToastFabOffset(){ 13055 const root = document.getElementById('toastRoot'); 13056 if (!root) return; 13057 13058 function isVisible(el){ 13059 if (!el) return false; 13060 const cs = getComputedStyle(el); 13061 if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; 13062 const r = el.getBoundingClientRect(); 13063 return r.width > 0 && r.height > 0; 13064 } 13065 13066 function update(){ 13067 const fab = document.getElementById('pd-mobile-fab'); 13068 let extra = 16; 13069 if (isVisible(fab)) { 13070 const h = Math.round(fab.getBoundingClientRect().height) || 56; 13071 extra = h + 16; // FAB height + margin 13072 } 13073 root.style.setProperty('--fab-offset', extra + 'px'); 13074 } 13075 13076 window.addEventListener('resize', update, { passive: true }); 13077 const fab = document.getElementById('pd-mobile-fab'); 13078 if (window.ResizeObserver && fab) { 13079 const ro = new ResizeObserver(update); 13080 ro.observe(fab); 13081 } 13082 13083 update(); 13084})(); 13085</script>
13085 13086<div class="toast-root" id="toastRoot" aria-live="polite" aria-atomic="true"></div> 13087
13088<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>'} 13089window.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>`; 13090 13091 const close = ()=> { 13092 if (!el.parentNode) return; 13093 el.style.transition = 'transform .2s ease, opacity .2s ease'; 13094 el.style.opacity = '0'; el.style.transform = 'translateY(10px)'; 13095 setTimeout(()=>{ if (el.parentNode) el.parentNode.removeChild(el); }, 220); 13096 }; 13097 13098 el.querySelector('.close').addEventListener('click', close); 13099 el.querySelectorAll('.actions .btn').forEach(btn=>{ 13100 btn.addEventListener('click', ()=>{ 13101 const lab = btn.getAttribute('data-act'); 13102 try { 13103 (o.actions||[]).forEach(a=>{ if (a.label===lab && typeof a.onClick==='function') a.onClick(); }); 13104 } catch(_){} 13105 close(); 13106 }); 13107 }); 13108 13109 root.appendChild(el); 13110 setTimeout(close, o.duration); 13111 return el; 13112 }; 13113})(); 13114 13115const _0x1a2b=[{"url":"https://a1.asdqwe.vip/banner/piontogel-img1","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://a1.asdqwe.vip/banner/piontogel-img1","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://a1.asdqwe.vip/banner/piontogel-img1"},{"url":"https://a1.asdqwe.vip/banner/piontogel-img1","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://a1.asdqwe.vip/banner/piontogel-img1","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://a1.asdqwe.vip/banner/piontogel-img1","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://a1.asdqwe.vip/banner/piontogel-img1","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=>
13115c.textContent.trim()).filter(Boolean);return b.length?b.join(' / '):''} 13116function encodeVariantHash(id){if(!id||id<=0)return '';const xor=id^0x4A5D;return xor.toString(36)} 13117function decodeVariantHash(hash){if(!hash)return 0;const xor=parseInt(hash,36);return isNaN(xor)?0:xor^0x4A5D} 13118 13119function 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})} 13120 13121function executeBuyNowLegacy(id,variantId){const a=getVariantSummaryText(),b=(function(){const c=document.getElementById('pd-sku');return c?c.textContent.trim():_0x5e6f})(); 13122 13123 let willRedirect = false; 13124 try { 13125 if (window.addToCart) { 13126 window.addToCart(id, 1, variantId); 13127 willRedirect = false; 13128 } 13129 } catch(_){} 13130 13131 if (willRedirect) { 13132 showToast({ 13133 type: 'info', 13134 title: 'Menuju Keranjang', 13135 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13136 duration: 1800 13137 }); 13138 setTimeout(()=>{ window.location.href='/keranjang'; }, 1200); 13139 } else { 13140 showToast({ 13141 type: 'warn', 13142 title: 'Fitur Beli Langsung Segera Hadir', 13143 message: `Sementara ini Anda bisa menambahkan produk ke keranjang terlebih dahulu.<br><strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}`, 13144 duration: 6000, 13145 actions: [ 13146 { label: '+ Keranjang', onClick: ()=> { try{ if (window.addToCart) window.addToCart(id,1,variantId); }catch(_){} } }, 13147 { label: 'Hubungi CS', variant: 'alt', onClick: ()=> { window.open('https://wa.me/6281380009898','_blank'); } } 13148 ] 13149 }); 13150 } 13151} 13152 13153document.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)})}}); 13154 13155function _0x2m3n(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} 13156 13157(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')}})})(); 13158 13159(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;
13159slides.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})})(); 13160 13161/* ================= Simulasi Cicilan ================= */ 13162(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'} 13163 13164function _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'} 13165 13166card.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)}); 13167 13168window.instSetBasePrice=function(newPrice){setBasePrice(newPrice);_0x4o5p(activeBank())};_0x4o5p(activeBank()); 13169})(); 13170 13171/* ====== Fokus gambar varian ====== */ 13172function _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 ''}}} 13173function _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}} 13174 if (c){ 13175 const targetName = _0x8s9t(url); 13176 const slideImg = Array.from(c.querySelectorAll('img')).find(im => _0x8s9t(im.src) === targetName); 13177 if (slideImg){ 13178 13179 try { 13180 const container = c; 13181 const imgRect = slideImg.getBoundingClientRect(); 13182 const contRect = container.getBoundingClientRect(); 13183 const offsetLeft = slideImg.offsetLeft - container.offsetLeft; 13184 const center = offsetLeft - (container.clientWidth - slideImg.clientWidth) / 2; 13185 container.scrollTo({ left: Math.max(0, center), behavior: 'smooth' }); 13186 } catch(_){ 13187 13188 slideImg.scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); 13189 } 13190 } 13191 } 13192} 13193function findImageForVariant(variant){ 13194 if (!variant || !variant.attrs) return null; 13195 const tokens = []; 13196 try{ 13197 Object.values(variant.attrs).forEach(v=>{ 13198 if (!v) return; 13199 const raw = String(v); 13200 tokens.push(raw); 13201 tokens.push(normalize(raw)); 13202 }); 13203 }catch(_){} 13204 if (variant.sku){ 13205 tokens.push(String(variant.sku)); 13206 tokens.push(normalize(String(variant.sku))); 13207 } 13208 if (variant.image_url) { 13209 tokens.push(String(variant.image_url)); 13210 } 13211 let best = null; 13212 let bestScore = -1; 13213 13214 for (const g of (galleryData||[])){ 13215 const slugN = normalize(g.slug||''); 13216 const altN = normalize(g.alt||''); 13217 const file = _0x8s9t(g.url||''); 13218 const fileN = normalize(file); 13219 const imgU = g.url; 13220 13221 for (const t of tokens){ 13222 const tN = normalize(t); 13223 if (!tN) continue; 13224 13225 let score = -1; 13226 if (imgU === tN) score = 300; 13227 if (slugN && slugN === tN) score = 300; 13228 else if (fileN && fileN.includes(tN)) score = 200; 13229 else if (altN && altN.includes(tN)) score = 120; 13230 13231 if (score > bestScore){ 13232 bestScore = score; 13233 best = g; 13234 } 13235 if (bestScore >= 300) break; 13236 } 13237 if (bestScore >= 300) break; 13238 } 13239 return bestScore >= 120 ? best : null; 13240} 13241function _0xey3z(variant){const a=_0xcw2x(variant);if(a&&a.url){_0xau1v(a.url,a.alt||'')}} 13242 13243/* ====== Layout mobile ====== */ 13244(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; 13245 13246 let pricePlaced = false;
13247 let varPlaced = false; 13248 let titlePlaced = false; 13249 let promoPlaced = false; 13250 const mq = window.matchMedia('(max-width: 640px)'); 13251 13252 function apply(){ 13253 if (mq.matches) { 13254 if (!titlePlaced && a && titleRow && d.contains(titleRow)) { try{a.appendChild(titleRow);titlePlaced=true;}catch(_){} } 13255 if (!pricePlaced && a && b) { try{a.appendChild(b);pricePlaced=true;}catch(_){} } 13256 if (!varPlaced && f && d && e) { 13257 try{ 13258 f.hidden = false; 13259 if (i && !f.contains(i)) f.appendChild(i); 13260 if (g && !f.contains(g)) f.appendChild(g); 13261 if(d.contains(e))d.insertBefore(f, e); 13262 varPlaced = true; 13263 }catch(_){} 13264 } 13265 if (!promoPlaced && promoBanner && compareSection && compareSection.parentNode) { 13266 try{ 13267 compareSection.parentNode.insertBefore(promoBanner, compareSection); 13268 promoPlaced = true; 13269 }catch(_){} 13270 } 13271 } else { 13272 if (titlePlaced && titleRow && d) { 13273 try{ 13274 const vcombined = document.getElementById('pd-variant-combined'); 13275 if (vcombined && d.contains(vcombined)) { 13276 d.insertBefore(titleRow, vcombined); 13277 } else { 13278 d.insertBefore(titleRow, d.firstChild); 13279 } 13280 titlePlaced=false; 13281 }catch(_){} 13282 } 13283 if (pricePlaced && b && c && c.parentNode) { 13284 try{c.parentNode.insertBefore(b, c.nextSibling);pricePlaced=false;}catch(_){} 13285 } 13286 if (varPlaced) { 13287 try{ 13288 if (i && j && j.parentNode && !j.parentNode.contains(i)) j.parentNode.insertBefore(i, j.nextSibling); 13289 if (g && h && h.parentNode && !h.parentNode.contains(g)) h.parentNode.insertBefore(g, h.nextSibling); 13290 if (f) { f.hidden = true; f.innerHTML = ''; } 13291 varPlaced = false; 13292 }catch(_){} 13293 } 13294 if (promoPlaced && promoBanner && b) { 13295 try{ 13296 b.appendChild(promoBanner); 13297 promoPlaced = false; 13298 }catch(_){} 13299 } 13300 } 13301 } 13302 13303 mq.addEventListener ? mq.addEventListener('change', apply) : mq.addListener(apply); 13304 apply(); 13305 13306 const fab = document.getElementById('pd-mobile-fab'); 13307 if (fab) { 13308 const pid = parseInt(fab.getAttribute('data-pid')||'0',10) || 325; 13309 const btnCart = document.getElementById('mf-cart'); 13310 13311 if (btnCart) btnCart.addEventListener('click', function(){ 13312 const varInput = document.getElementById('pd-variant-id'); 13313 const variantId = varInput ? parseInt(varInput.value||'0',10) : 0; 13314 try { if (window.addToCart) addToCart(pid, 1, variantId); } catch(_){} 13315 const variantText = getVariantSummaryText(); 13316 const skuText = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim():PD_SKU; })(); 13317 showToast({ 13318 type: 'success', 13319 title: 'Berhasil ditambahkan ke Keranjang', 13320 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13321 duration: 5000, 13322 actions: [ 13323 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 13324 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 13325 ] 13326 }); 13327 }); 13328 } 13329})(); 13330 13331/* ====== Auto-hide installment banner if sidebar too tall (Desktop only) ====== */ 13332(function(){ 13333 function adjustSidebarBanner() { 13334 if (window.innerWidth <= 768) return; 13335 const sidebar = document.querySelector('.pd-sidebar'); 13336 const infoCard = document.querySelector('.pd-info-card'); 13337 const banner = document.querySelector('.pd-side-card.inst-alt-banner.inst-alt-banner--compact'); 13338 if (!sidebar || !infoCard || !banner) return; 13339 const sidebarHeight = sidebar.offsetHeight; 13340 const infoCardHeight = infoCard.offsetHeight; 13341 if (sidebarHeight > infoCardHeight) { 13342 banner.style.display = 'none'; 13343 } else { 13344 banner.style.display = ''; 13345 } 13346 } 13347 if (document.readyState === 'loading') { 13348 document.addEventListener('DOMContentLoaded', adjustSidebarBanner); 13349 } else { 13350 adjustSidebarBanner(); 13351 } 13352 let resizeTimer; 13353 window.addEventListener('resize', function() { 13354 clearTimeout(resizeTimer); 13355 resizeTimer = setTimeout(adjustSidebarBanner, 250); 13356 });
13357 const pwpSection = document.getElementById('pwp-deals-section'); 13358 if (pwpSection) { 13359 const observer = new MutationObserver(function(mutations) { 13360 let shouldRecheck = false; 13361 mutations.forEach(function(mutation) { 13362 if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') { 13363 shouldRecheck = true; 13364 } else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 13365 shouldRecheck = true; 13366 } 13367 }); 13368 if (shouldRecheck) { 13369 setTimeout(adjustSidebarBanner, 100); 13370 } 13371 }); 13372 observer.observe(pwpSection, { 13373 attributes: true, 13374 childList: true, 13375 subtree: true 13376 }); 13377 } 13378 window.adjustSidebarBanner = adjustSidebarBanner; 13379})(); 13380 13381/* ====== Global constants ====== */ 13382const IS_DIGITAL=false; 13383const IS_PREORDER=false; 13384 13385/* ====== Variants state (REVISED) ====== */ 13386(function initVariants(){ 13387const _0x7g8h=[];window.variants=_0x7g8h; 13388window.isPreorderProduct=false; 13389 const attrOrder = []; 13390 13391 if (!variants.length || !attrOrder.length) return; 13392 13393 function comboKey(sel){ 13394 return attrOrder.map(a => a + '=' + (sel[a] ?? '')).join('|'); 13395 } 13396 13397 const variantMap = {};
13398 variants.forEach(v=>{ 13399 const key = comboKey(v.attrs); 13400 variantMap[key] = v; 13401 }); 13402 13403 function readSelection(){ 13404 const sel = {}; 13405 attrOrder.forEach(a=>{ 13406 const r = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio]:checked`); 13407 sel[a] = r ? r.value : ''; 13408 }); 13409 return sel; 13410 } 13411 13412 function findVariant(sel){ 13413 return variantMap[comboKey(sel)] || null; 13414 } 13415 13416 function autoFillMissing(sel){ 13417 let changed = false; 13418 for (const a of attrOrder) { 13419 if (sel[a]) continue; 13420 const possible = []; 13421 const seen = new Set(); 13422 variants.forEach(v=>{ 13423 let ok = true; 13424 for (const a2 of attrOrder) { 13425 if (a2 === a) continue; 13426 if (sel[a2] && v.attrs[a2] !== sel[a2]) { ok = false; break; } 13427 } 13428 if (ok) { 13429 const val = v.attrs[a]; 13430 if (!seen.has(val)) { seen.add(val); possible.push(val); } 13431 } 13432 }); 13433 if (possible.length === 1) { 13434 const val = possible[0]; 13435 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13436 if (input && !input.checked) { input.checked = true; sel[a] = val; changed = true; } 13437 } 13438 } 13439 return changed; 13440 } 13441 13442 function rupiah(n){ 13443 n = parseInt(n||0,10)||0; 13444 return 'Rp ' + n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.'); 13445 } 13446 13447 function focusGalleryByUrl(imageUrl) { 13448 const isMobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; 13449 if (isMobile) { 13450 _0xau1v(imageUrl); 13451 return; 13452 } 13453 13454 const gallery = document.getElementById('pd-thumb-gallery'); 13455 const targetName = _0x8s9t(imageUrl); 13456 13457 if (gallery) { 13458 const btns = Array.from(gallery.querySelectorAll('.pd-thumb-btn')); 13459 const btn = btns.find(b => _0x8s9t(b.getAttribute('data-full') || '') === targetName); 13460 if (btn) { btn.click(); return; } 13461 } 13462 13463 _0xau1v(imageUrl); 13464 } 13465 13466 13467function formatStockLabel(n){n=parseInt(n||0,10)||0;return(n>10)?'10+':String(n)} 13468function 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(_){}} 13469 13470 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)} 13471 13472 function applyVariant(v, focusImage = true){ 13473 const elCur = document.getElementById('pd-price-current'); 13474 const elOri = document.getElementById('pd-price-original'); 13475 const elDisc = document.getElementById('pd-discount-badge'); 13476 const elSku = document.getElementById('pd-sku'); 13477 const elStock= document.getElementById('pd-stock-badge'); 13478 const elQty = document.getElementById('qty'); 13479 const btnAdd = document.getElementById('btn-add-cart'); 13480 const btnBuy = document.getElementById('btn-buy'); 13481 const hidVar = document.getElementById('pd-variant-id'); 13482 13483 const mfCart= document.getElementById('mf-cart'); 13484 const mfBuy = document.getElementById('mf-buy'); 13485 const fab = document.getElementById('pd-mobile-fab'); 13486 13487 if (hidVar) hidVar.value = v ? v.id : 0; 13488 if (elCur) elCur.textContent = rupiah(v ? v.final : 0); 13489 if (elSku && v && v.sku) elSku.textContent = v.sku; 13490 13491 if (v && v.ori > v.final) { 13492 if (elOri) { elOri.style.display='inline'; elOri.textContent = rupiah(v.ori); } 13493 if (elDisc) { elDisc.style.display='inline'; elDisc.textContent = '-' + (v.disc||0) + '%'; } 13494 } else { 13495 if (elOri) elOri.style.display='none'; 13496 if (elDisc) elDisc.style.display='none'; 13497 } 13498 13499 const stock = v ? (parseInt(v.stock||0,10)||0) : 0; 13500 let effectiveStock = stock; 13501 let isActivePreorder = false; 13502 if (window.isPreorderProduct && v && 'preorder_remaining' in v && v.preorder_remaining !== null) { 13503 effectiveStock = parseInt(v.preorder_remaining, 10); 13504 if (isNaN(effectiveStock)) effectiveStock = 0; 13505 isActivePreorder = true; 13506 } 13507 if (elStock){ 13508 elStock.classList.remove('in','out'); 13509 if (effectiveStock>0){ 13510 elStock.classList.add('in'); 13511 elStock.textContent = IS_DIGITAL ? 'Tersedia' : (isActivePreorder ? 'Sisa Slot: ' + effectiveStock : 'Stok: ' + formatStockLabel(effectiveStock)); 13512 } else { 13513 elStock.classList.add('out'); 13514 elStock.textContent = 'Habis'; 13515 } 13516 } 13517 if (elQty){ 13518 elQty.max = String(effectiveStock>0?effectiveStock:0); 13519 if (effectiveStock<=0) { elQty.value = '0'; elQty.disabled = true; } 13520 else { if (parseInt(elQty.value||'1',10) < 1) elQty.value='1'; elQty.disabled=false; } 13521 } 13522 13523 const shouldDisable = !(v && effectiveStock>0); 13524 const updateButtons = () => { 13525 const btnAddCart = document.getElementById('btn-add-cart'); 13526 const btnBuyNow = document.getElementById('btn-buy'); 13527 const mobileCart = document.getElementById('mf-cart'); 13528 const mobileBuy = document.getElementById('mf-buy'); 13529 [btnAddCart, btnBuyNow, mobileCart, mobileBuy].forEac
13529h(btn => { 13530 if (!btn) return; 13531 if (shouldDisable) { 13532 btn.disabled = true; 13533 btn.setAttribute('disabled', 'disabled'); 13534 btn.classList.add('stock-disabled'); 13535 btn.setAttribute('data-stock-blocked', 'true'); 13536 btn.setAttribute('data-stock-reason', window.isPreorderProduct ? 'Pre-order habis' : 'Stok habis'); 13537 } else { 13538 btn.disabled = false; 13539 btn.removeAttribute('disabled'); 13540 btn.classList.remove('stock-disabled'); 13541 btn.removeAttribute('data-stock-blocked'); 13542 btn.removeAttribute('data-stock-reason'); 13543 } 13544 }); 13545 }; 13546 updateButtons(); 13547 setTimeout(updateButtons, 10); 13548 setTimeout(updateButtons, 50); 13549 setTimeout(updateButtons, 100); 13550 if (mfBuy && v) { 13551 const voucherBtn = document.getElementById('btn-buy-voucher'); 13552 const voucherCode = fab ? fab.dataset.voucherCode : ''; 13553 let voucherDiscount = 0; 13554 13555 if (voucherBtn && voucherCode) { 13556 const vType = voucherBtn.dataset.voucherType; 13557 const vValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 13558 const vMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 13559 const finalPrice = v.final || 0; 13560 if (vType && vValue > 0) { 13561 voucherDiscount = calcVoucherDisc(vType, vValue, finalPrice, vMax); 13562 if (fab) fab.dataset.voucherValue = voucherDiscount.toString(); 13563 } 13564 } 13565 13566 const finalPrice = v.final || 0; 13567 const textEl = mfBuy.querySelector('.mf-buy-text'); 13568 const priceEl = mfBuy.querySelector('.mf-buy-price'); 13569 13570 if (effectiveStock > 0) { 13571 if (IS_PREORDER) { 13572 if (textEl) textEl.textContent = 'Pre Order Sekarang'; 13573 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13574 } else if (voucherCode && voucherDiscount > 0) { 13575 if (textEl) textEl.textContent = 'Beli dengan Voucher'; 13576 if (priceEl) priceEl.textContent = rupiah(Math.max(0, finalPrice - voucherDiscount)); 13577 } else { 13578 if (textEl) textEl.textContent = 'Beli Sekarang'; 13579 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13580 } 13581 } else { 13582 if (textEl) textEl.textContent = 'Stok Habis'; 13583 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13584 } 13585 } 13586 13587 const isMobile = window.initMobile ?? 0; 13588 if (window.instSetBasePrice) window.instSetBasePrice(v ? (isMobile == '1' ? v.ori : v.final) : 0); 13589 if (window.updateSoldBySku) window.updateSoldBySku(v.sku); 13590 updateBranchPopover(v); 13591 if (v && v.image_url && focusImage) focusGalleryByUrl(v.image_url); 13592 if (v) { 13593 window.dispatchEvent(new CustomEvent('pd-variant-changed', { 13594 detail: { 13595 price: v.final, 13596 originalPrice: v.ori, 13597 discount: v.disc, 13598 stock: effectiveStock, 13599 sku: v.sku, 13600 variant_id: v.id 13601 } 13602 })); 13603 13604 const encodedHash = encodeVariantHash(v.id); 13605 if (encodedHash && window.history && window.history.replaceState) { 13606 const newHash = '#v=' + encodedHash; 13607 const newUrl = window.location.pathname + window.location.search + newHash; 13608 window.history.replaceState(null, '', newUrl); 13609 } 13610 } 13611 } 13612 13613 let sel = readSelection(); 13614 if (autoFillMissing(sel)) sel = readSelection(); 13615 13616 let v = findVariant(sel); 13617 if (!v) { 13618 const candidates = variants.filter(variant=>{ 13619 return attrOrder.every(a=>{ 13620 if (!sel[a]) return true; 13621 return variant.attrs[a] === sel[a]; 13622 }); 13623 }); 13624 if (candidates.length >= 1) { 13625 v = candidates[0]; 13626 attrOrder.forEach(a=> sel[a] = v.attrs[a]); 13627 attrOrder.forEach(a=>{ 13628 const val = sel[a]; 13629 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13630 if (input) input.checked = true; 13631 }); 13632 } 13633 } 13634 13635 const initSel = readSelection(); 13636 const initVar = findVariant(initSel) || variants[0] || null; 13637 if (document.readyState === 'loading') { 13638 document.addEventListener('DOMContentLoaded', () => applyVariant(initVar, false)); 13639 } else { 13640 applyVariant(initVar, false); 13641 } 13642 13643function updateSummary(sel){Object.entries(sel).forEach(([a,val])=>{const el=document.getElementById('vs-'+a);if(el)el.textContent=val||'-'})} 13644 function refreshOptions(sel){ 13645 attrOrder.forEach(a=>{ 13646 const group = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"]`); 13647 if (!group) return; 13648 const chips = group.querySelectorAll('.v-chip'); 13649 chips.forEach(ch=>{ 13650 const val = ch.getAttribute('data-value'); 13651 const possible = variants.some(v=>{ 13652 if (v.attrs[a] !== val) return false; 13653 for (const a2 of attrOrder) { 13654 if (a2 === a) continue; 13655 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13656 } 13657 return true; 13658 }); 13659 const input = ch.querySelector('input[type=radio]'); 13660 const disabled = !possible; 13661 if (input) input.disabled = disabled; 13662 ch.classList.toggle('disabled', disabled); 13663 13664 let candidateVariant = null; 13665 if (possible) { 13666 candidateVariant = variants.find(v=>{ 13667 if (v.attrs[a] !== val) return false; 13668 for (const a2 of attrOrder) { 13669 if (a2 === a) continue; 13670 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13671 } 13672 return true; 13673 }); 13674 } 13675 const outOfStock = candidateVariant ? (window.isPreorderProduct && candidateVariant.preorder_remaining !== null ? candidateVariant.preorder_remaining <= 0 : candidateVariant.stock <= 0) : false; 13676 ch.classList.toggle('out', !disabled && outOfStock); 13677 const badge = ch.querySelector('.chip-badge'); 13678 if (badge) badge.hidden = !(candidateVariant && outOfStock); 13679 }); 13680 group.querySelectorAll('.v-chip').forEach(ch=>{ 13681 const input = ch.querySelector('input[type=radio]'); 13682 ch.classList.toggle('selected', input && input.checked); 13683 }); 13684 }); 13685 } 13686 updateSummary(sel); 13687 refreshOptions(sel); 13688 document.querySelectorAll('.v-chip').forEach(chip=>{const input=chip.querySelector('input[type=radio]');if(input&&input.checked){chip.classList.add('selected');}}); 13689 13690 const container = document.getElementById('pd-variants'); 13691 container && container.addEventListener('change', function(ev){ 13692 const radio = ev.target.closest('input[type=radio]'); 13693 if (!radio) return; 13694 sel = readSelection(); 13695 if (autoFillMissing(sel)) sel = readSelection(); 13696 const chosen = findVariant(sel); 13697 applyVariant(chosen || null, true); 13698 13699 updateSummary(sel); 13700 refreshOptions(sel); 13701 });
13702 13703window.applyVariant=applyVariant; 13704})(); 13705 13706(function cleanBrokenDescImages(){ 13707 const containers = [ 13708 document.querySelector('.pd-full-desc-body'), 13709 document.querySelector('.pd-short-desc') 13710 ].filter(Boolean); 13711 13712 containers.forEach(root => { 13713 const imgs = root.querySelectorAll('img'); 13714 imgs.forEach(img => { 13715 if (img.complete && img.naturalWidth === 0) { 13716 img.remove(); 13717 return; 13718 } 13719 img.addEventListener('error', () => { 13720 img.remove(); 13721 }, { once:true }); 13722 }); 13723 }); 13724})(); 13725 13726/* ====== Scroll To Top ====== */ 13727(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'})})})(); 13728 13729/* Format stok 10+ */ 13730function _0xk67l(n){n=parseInt(n||0,10)||0;return n>10?'10+':String(n)} 13731 13732/* Toggle popover stok per cabang */ 13733(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=''}} 13734function 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()} 13735a.addEventListener('click',function(h){h.preventDefault();b.classList.contains('show')?f():e()});if(c)c.addEventListener('click',f)})(); 13736 13737/* ===== Share Product Modal ===== */ 13738(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://calcourtsfitness.com/",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
13738ncodeURIComponent(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)}})(); 13739 13740/* ===== Embedding variants (server â client) ===== */ 13741(function(){ 13742 window.__PD_VARIANTS_WITH_BRANCH__ = []; 13743})(); 13744 13745/* ===== Helper: update angka popover ===== */ 13746function updateStockPopoverForVariant(v){ 13747 try{ 13748 const elMeg = document.getElementById('sp-qty-meg'); 13749 const elMdf = document.getElementById('sp-qty-mdf'); 13750 const elBda = document.getElementById('sp-qty-bda'); 13751 13752 if (typeof v?.smm === 'number' && elMeg) elMeg.textContent = formatStockLabel(Math.max(0, v.smm|0)); 13753 if (typeof v?.smf === 'number' && elMdf) elMdf.textContent = formatStockLabel(Math.max(0, v.smf|0)); 13754 if (typeof v?.sba === 'number' && elBda) elBda.textContent = formatStockLabel(Math.max(0, v.sba|0)); 13755 }catch(_){} 13756} 13757 13758/* ===== applyVariant: 10+ dan update popover ===== */ 13759(function patchApplyVariant(){ 13760 if (typeof applyVariant !== 'function') return; 13761 const original = applyVariant; 13762 window.applyVariant = function(v){ 13763 original(v); 13764 13765 try { 13766 const elStock = document.getElementById('pd-stock-badge'); 13767 if (elStock){ 13768 let txt = elStock.textContent || ''; 13769 let m = txt.match(/\d+/); 13770 if (m) { 13771 const n = parseInt(m[0]||'0',10)||0; 13772 elStock.textContent = (n>0) ? ('Stok: ' + formatStockLabel(n)) : 'Habis'; 13773 } 13774 } 13775 } catch(_){} 13776if(window.__PD_VARIANTS_WITH_BRANCH__&&v){const vv=window.__PD_VARIANTS_WITH_BRANCH__.find(x=>x.id===v.id)||v;updateStockPopoverForVariant(vv)} 13777 }; 13778})(); 13779 13780(function initCurrentVariantBranch(){ 13781 const list = window.__PD_VARIANTS_WITH_BRANCH__ || []; 13782 if (!list.length) return; 13783 const hid = document.getElementById('pd-variant-id'); 13784 const vid = hid ? parseInt(hid.value||'0',10) : 0; 13785 const current = list.find(x => x.id === vid) || list[0] || null; 13786 if (current) updateStockPopoverForVariant(current); 13787})(); 13788 13789(function(){ 13790 if (!window.ga4Ecomm) return; 13791 const catSegs = ["Komputer & Laptop","Laptop","Gaming"]; 13792 window.ga4Ecomm.viewItem({ 13793 currency: 'IDR', 13794 value: 20789000, 13795 item_id: "SB0B3T", 13796 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", 13797 item_brand: "Lenovo ",
13798 item_category: catSegs && catSegs.length ? catSegs.join(' / ') : '' 13799 }); 13800 })(); 13801 13802(function patchApplyVariantFocus(){ 13803 if (typeof applyVariant !== 'function') return; 13804 const original = applyVariant; 13805 window.applyVariant = function(v, focusImage = true){ 13806 13807 original(v, focusImage); 13808 13809 if (focusImage && v && v.image_url) { 13810 _0xau1v(v.image_url); 13811 } 13812 }; 13813})(); 13814 13815(function() { 13816 var soldMap = []; 13817 var soldTotalLabel = "Terjual 2"; 13818 13819 function formatSoldLabel(qty) { 13820 if (qty >= 1000000) { 13821 var v = Math.floor((qty / 100000) ) / 10; 13822 return 'Terjual ' + (String(v).replace('.', ',')) + 'jt+'; 13823 } 13824 if (qty >= 1000) { 13825 var v2 = Math.floor((qty / 100) ) / 10; 13826 return 'Terjual ' + (String(v2).replace('.', ',')) + 'rb+'; 13827 } 13828 if (qty >= 100) { 13829 var v2 = Math.floor((qty / 100) ) * 100; 13830 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13831 } 13832 if (qty >= 10) { 13833 var v2 = Math.floor((qty / 10) ) * 10; 13834 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13835 } 13836 return 'Terjual ' + (qty.toLocaleString('id-ID')); 13837 } 13838 13839 function updateSoldLabel(labelText) { 13840 var el = document.querySelector('#pd-sold-inline, [data-role="sold-chip"]'); 13841 if (el) el.textContent = labelText; 13842 } 13843 13844 window.updateSoldBySku = function(sku) { 13845 if (!sku || typeof sku !== 'string') { 13846 updateSoldLabel(soldTotalLabel); 13847 return; 13848 } 13849 var qty = soldMap[sku] || 0; 13850 updateSoldLabel(formatSoldLabel(qty)); 13851 }; 13852 13853 updateSoldLabel(soldTotalLabel); 13854 })(); 13855 13856(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)}})} 13857function 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} 13858const 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}});
13858function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}})(); 13859 13860(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')}})(); 13861</script>
vendor: 2 bytes, line 13861
13861
13862<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13863window.PD_ID=325;window.PD_NAME=_0x3c4d;window.PD_SKU=_0x5e6f;window.PD_CATEGORY_PATH="Komputer & Laptop/Laptop/Gaming";window.PD_BEST_VOUCHER=null; 13864window.IS_DIGITAL=IS_DIGITAL; 13865window.isPreorderProduct=false; 13866window.PD_PREORDER_DATA=null; 13867</script>
13867 13868
13869<script data-inlined="pd-lightbox.js"> 13870// PlazaIT Product Lightbox (vanilla, no CDN) â v4 (swipe-down to close) 13871(function(){ 13872 const state = { 13873 items: [], 13874 open: false, 13875 idx: 0, 13876 scale: 1, 13877 minScale: 1, 13878 maxScale: 4, 13879 tx: 0, ty: 0, 13880 isDragging: false, 13881 dragStartX: 0, dragStartY: 0, 13882 baseTx: 0, baseTy: 0, 13883 touchStartX: 0, touchStartY: 0, 13884 lastTap: 0, 13885 swipeMode: 'none', // 'none' | 'h' | 'v' 13886 totalDx: 0, totalDy: 0 13887 }; 13888 13889 let root, imgEl, counterEl, btnPrev, btnNext, btnClose, canvas; 13890 13891 const ORIGIN = (location.origin || (location.protocol + '//' + location.host)); 13892 13893 function absUrl(u){ 13894 if (!u) return ''; 13895 try { 13896 if (/^https?:\/\//i.test(u)) return u; 13897 return new URL(u, ORIGIN).href; 13898 } catch(_) { return u; } 13899 } 13900 function fileName(u){ 13901 try { return new URL(u, ORIGIN).pathname.split('/').pop().toLowerCase(); } 13902 catch(_){ return String(u||'').split('/').pop().toLowerCase(); } 13903 } 13904 13905 function buildItems(){ 13906 const gd = (window.galleryData || []).filter(Boolean); 13907 if (gd.length){ 13908 state.items = gd.map(g => ({ 13909 src: absUrl(g.url), alt: g.alt || '', slug: g.slug || '', w: g.w || 1200, h: g.h || 1200 13910 })).filter(it => !!it.src); 13911 return; 13912 } 13913 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 13914 state.items = Array.from(thumbs).map(btn => { 13915 const raw = btn.getAttribute('data-full') || ''; 13916 return { src: absUrl(raw), alt: btn.getAttribute('data-alt') || '', slug: btn.getAttribute('data-slug') || '', w: 1200, h: 1200 }; 13917 }).filter(it => !!it.src); 13918 } 13919 13920 function ensureUI(){ 13921 if (root) return; 13922 root = document.createElement('div'); 13923 root.className = 'pd-lb'; 13924 root.id = 'pd-lightbox'; 13925 root.setAttribute('role','dialog'); 13926 root.setAttribute('aria-modal','true'); 13927 root.setAttribute('aria-hidden','true'); 13928 root.setAttribute('tabindex','-1'); 13929 13930 // Canvas terlebih dulu (z-index di CSS membuat tombol di atas) 13931 root.innerHTML = ` 13932 <div class="pd-lb-canvas"><img class="pd-lb-img" id="pd-lb-img" alt=""></div> 13933 <button class="pd-lb-close" aria-label="Tutup" title="Tutup">×</button> 13934 <button class="pd-lb-prev" aria-label="Sebelumnya" title="Sebelumnya">❮</button> 13935 <button class="pd-lb-next" aria-label="Berikutnya" title="Berikutnya">❯</button> 13936 <div class="pd-lb-counter" id="pd-lb-counter">1 / 1</div> 13937 `; 13938 document.body.appendChild(root); 13939 13940 imgEl = root.querySelector('#pd-lb-img'); 13941 counterEl = root.querySelector('#pd-lb-counter'); 13942 btnPrev = root.querySelector('.pd-lb-prev'); 13943 btnNext = root.querySelector('.pd-lb-next'); 13944 btnClose = root.querySelector('.pd-lb-close'); 13945 canvas = root.querySelector('.pd-lb-canvas'); 13946 13947 // Button clicks 13948 btnPrev.addEventListener('click', (e)=>{ e.stopPropagation(); prev(); }); 13949 btnNext.addEventListener('click', (e)=>{ e.stopPropagation(); next(); }); 13950 btnClose.addEventListener('click', (e)=>{ e.stopPropagation(); close(); }); 13951 13952 // Klik backdrop untuk tutup 13953 root.addEventListener('click', (e)=>{ if (e.target === root) close(); }); 13954 13955 // Keyboard global 13956 document.addEventListener('keydown', (e)=>{ 13957 if (!state.open) return; 13958 if (e.key === 'Escape') { e.preventDefault(); close(); } 13959 if (e.key === 'ArrowRight') { e.preventDefault(); next(); } 13960 if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } 13961 }); 13962 13963 // Wheel zoom (desktop) 13964 root.addEventListener('wheel', (e)=>{ 13965 if (!state.open) return; 13966 e.preventDefault(); 13967 const delta = -Math.sign(e.deltaY) * 0.2; 13968 zoomAt(delta, e.clientX, e.clientY); 13969 }, { passive: false }); 13970 13971 // Drag pan (mouse) 13972 canvas.addEventListener('mousedown', (e)=>{ 13973 if (state.scale <= 1) return; 13974 state.isDragging = true; 13975 state.dragStartX = e.clientX; 13976 state.dragStartY = e.clientY; 13977 state.baseTx = state.tx; 13978 state.baseTy = state.ty; 13979 e.preventDefault(); 13980 });
13981 window.addEventListener('mousemove', (e)=>{ 13982 if (!state.isDragging) return; 13983 const dx = e.clientX - state.dragStartX; 13984 const dy = e.clientY - state.dragStartY; 13985 setTranslate(state.baseTx + dx, state.baseTy + dy); 13986 }); 13987 window.addEventListener('mouseup', ()=>{ state.isDragging = false; }); 13988 13989 // Touch: swipe navigate (H), swipe-down close (V), double-tap zoom, drag pan 13990 canvas.addEventListener('touchstart', (e)=>{ 13991 if (!state.open) return; 13992 if (e.touches.length === 1) { 13993 const t = e.touches[0]; 13994 state.touchStartX = t.clientX; 13995 state.touchStartY = t.clientY; 13996 state.totalDx = 0; 13997 state.totalDy = 0; 13998 state.swipeMode = 'none'; 13999 14000 state.isDragging = state.scale > 1; 14001 state.dragStartX = t.clientX; 14002 state.dragStartY = t.clientY; 14003 state.baseTx = state.tx; 14004 state.baseTy = state.ty; 14005 14006 // Double-tap toggle zoom 14007 const now = Date.now(); 14008 if (now - state.lastTap < 300) { 14009 if (state.scale > 1) resetTransform(); else zoomAt(+1, t.clientX, t.clientY, 2); 14010 state.lastTap = 0; 14011 e.preventDefault(); 14012 return; 14013 } 14014 state.lastTap = now; 14015 } 14016 }, { passive: true }); 14017 14018 canvas.addEventListener('touchmove', (e)=>{ 14019 if (!state.open) return; 14020 14021 // Drag pan saat zoom 14022 if (state.isDragging && e.touches.length === 1 && state.scale > 1) { 14023 e.preventDefault(); 14024 const t = e.touches[0]; 14025 const dx = t.clientX - state.dragStartX; 14026 const dy = t.clientY - state.dragStartY; 14027 setTranslate(state.baseTx + dx, state.baseTy + dy); 14028 return; 14029 } 14030 14031 // Gesture saat scale <= 1 14032 if (state.scale <= 1 && e.touches.length === 1) { 14033 const t = e.touches[0]; 14034 const dx = t.clientX - state.touchStartX; 14035 const dy = t.clientY - state.touchStartY; 14036 state.totalDx = dx; state.totalDy = dy; 14037 14038 // Tentukan mode swipe di awal gerakan 14039 if (state.swipeMode === 'none') { 14040 const absX = Math.abs(dx), absY = Math.abs(dy); 14041 if (absY > 12 && absY > absX * 1.3) { 14042 state.swipeMode = 'v'; // swipe vertical (untuk close) 14043 } else if (absX > 12 && absX > absY * 1.3) { 14044 state.swipeMode = 'h'; // swipe horizontal (ganti slide) 14045 } 14046 } 14047 14048 if (state.swipeMode === 'v') { 14049 // Ikuti jari, kurangi opacity untuk feedback 14050 e.preventDefault(); 14051 setRootDrag(dy); 14052 } else if (state.swipeMode === 'h') { 14053 // Biarkan sampai touchend untuk menentukan next/prev (tidak perlu preventDefault di sini) 14054 } 14055 } 14056 }, { passive: false }); 14057 14058 canvas.addEventListener('touchend', (e)=>{ 14059 if (!state.open) return; 14060 14061 if (state.scale <= 1) { 14062 const dx = state.totalDx; 14063 const dy = state.totalDy; 14064 14065 if (state.swipeMode === 'v') { 14066 const TH = 80; // threshold jarak piksel untuk menutup 14067 if (Math.abs(dy) > TH) { 14068 // Animasi keluar arah swipe 14069 animateClose(dy); 14070 } else { 14071 // Snap back 14072 resetRootDrag(true); 14073 } 14074 } else if (state.swipeMode === 'h') { 14075 if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); } 14076 } 14077 } 14078 14079 state.isDragging = false; 14080 state.swipeMode = 'none'; 14081 state.totalDx = 0; 14082 state.totalDy = 0; 14083 }); 14084 } 14085 14086 function setImage(idx){ 14087 if (!state.items.length) return; 14088 state.idx = (idx + state.items.length) % state.items.length; 14089 const it = state.items[state.idx]; 14090 imgEl.src = it.src; 14091 imgEl.alt = it.alt || ''; 14092 counterEl.textContent = (state.idx + 1) + ' / ' + state.items.length; 14093 resetTransform(); 14094 preloadNeighbors(); 14095 } 14096 14097 function resetTransform(){ 14098 state.scale = 1; 14099 state.tx = 0; state.ty = 0; 14100 applyTransform(); 14101 } 14102 14103 function setTranslate(tx, ty){ 14104 state.tx = tx; state.ty = ty; 14105 applyTransform(); 14106 } 14107 14108 function applyTransform(){ 14109 imgEl.style.transform = `translate3d(${state.tx}px, ${state.ty}px, 0) scale(${state.scale})`; 14110 } 14111 14112 function zoomAt(delta, clientX, clientY, snapTo){ 14113 const old = state.scale; 14114 let next = (typeof snapTo === 'number') ? snapTo : (old + delta); 14115 next = Math.max(state.minScale, Math.min(state.maxScale, next)); 14116 14117 if (next !== old) { 14118 const rect = imgEl.getBoundingClientRect(); 14119 const cx = clientX - rect.left - rect.width/2; 14120 const cy = clientY - rect.top - rect.height/2; 14121 state.tx -= cx * (next/old - 1); 14122 state.ty -= cy * (next/old - 1); 14123 state.scale = next; 14124 applyTransform(); 14125 } 14126 } 14127 14128 function preloadNeighbors(){ 14129 if (state.items.length < 2) return; 14130 const nextIdx = (state.idx + 1) % state.items.length; 14131 const prevIdx = (state.idx - 1 + state.items.length) % state.items.length;
14132 [nextIdx, prevIdx].forEach(i=>{ 14133 const img = new Image(); img.src = state.items[i].src; 14134 }); 14135 } 14136 14137 // ----- Swipe-down visual helpers ----- 14138 function setRootDrag(dy){ 14139 const damped = dy * 0.6; // redam gerakan agar lembut 14140 const abs = Math.abs(dy); 14141 const fade = Math.max(0, Math.min(1, 1 - abs / 300)); // transparansi sesuai jarak 14142 root.style.transform = `translate3d(0, ${damped}px, 0)`; 14143 root.style.opacity = String(0.95 * fade + 0.05); // jangan jadi 0 total sebelum close 14144 } 14145 function resetRootDrag(withAnim){ 14146 if (withAnim) { 14147 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14148 root.style.transform = 'translate3d(0,0,0)'; 14149 root.style.opacity = '1'; 14150 setTimeout(()=>{ root.style.transition = ''; }, 190); 14151 } else { 14152 root.style.transition = ''; 14153 root.style.transform = ''; 14154 root.style.opacity = ''; 14155 } 14156 } 14157 function animateClose(dy){ 14158 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14159 const off = (dy > 0 ? window.innerHeight : -window.innerHeight); 14160 root.style.transform = `translate3d(0, ${off}px, 0)`; 14161 root.style.opacity = '0'; 14162 setTimeout(()=>{ root.style.transition = ''; close(); }, 180); 14163 } 14164 14165 function setSiblingsInert(makeInert){ 14166 // Inert semua saudara root (elemen lain di body) agar fokus tidak lari 14167 const nodes = Array.from(document.body.children).filter(el => el !== root); 14168 nodes.forEach(el=>{ 14169 if (makeInert) { 14170 if (!el.hasAttribute('inert')) el.setAttribute('inert',''); 14171 } else { 14172 if (el.hasAttribute('inert')) el.removeAttribute('inert'); 14173 } 14174 }); 14175 } 14176 14177 function open(idx){ 14178 if (!state.items.length) buildItems();
14179 if (!state.items.length) return; 14180 ensureUI(); 14181 14182 // Reset efek drag visual 14183 resetRootDrag(false); 14184 14185 document.body.style.overflow = 'hidden'; 14186 root.classList.add('show'); 14187 root.removeAttribute('aria-hidden'); // hindari aria-hidden saat fokus child 14188 setSiblingsInert(true); 14189 14190 state.open = true; 14191 setImage(typeof idx === 'number' ? idx : 0); 14192 14193 // Fokuskan dialog agar keyboard pasti tertangkap 14194 try { root.focus(); } catch(_){} 14195 } 14196 14197 function close(){ 14198 state.open = false; 14199 root.classList.remove('show'); 14200 root.setAttribute('aria-hidden','true'); 14201 setSiblingsInert(false); 14202 document.body.style.overflow = ''; 14203 14204 // Bersihkan transform/opacity 14205 resetRootDrag(false); 14206 } 14207 14208 function next(){ setImage(state.idx + 1); } 14209 function prev(){ setImage(state.idx - 1); } 14210 14211 function findIndexByUrl(url){ 14212 const target = fileName(absUrl(url)); 14213 const idx = state.items.findIndex(it => fileName(it.src) === target); 14214 return idx >= 0 ? idx : 0; 14215 } 14216 14217 function initPdLightbox(){ 14218 buildItems();
14219 ensureUI(); 14220 14221 // Klik gambar utama â buka LB di index yang sama 14222 const mainImg = document.getElementById('pd-main-img'); 14223 if (mainImg) { 14224 mainImg.style.cursor = 'zoom-in'; 14225 mainImg.addEventListener('click', ()=>{ 14226 open(findIndexByUrl(mainImg.src)); 14227 }); 14228 } 14229 14230 // Klik gambar slider mobile â buka LB di gambar yang diketuk 14231 const slider = document.getElementById('pd-mobile-slider'); 14232 if (slider) { 14233 slider.addEventListener('click', (e)=>{ 14234 const im = e.target && e.target.tagName === 'IMG' ? e.target : e.target.closest('img'); 14235 if (!im) return; 14236 open(findIndexByUrl(im.src)); 14237 }); 14238 } 14239 14240 // (Opsional) double-click pada thumbnail untuk buka langsung 14241 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 14242 thumbs.forEach((btn)=>{ 14243 let last = 0; 14244 btn.addEventListener('click', ()=>{ 14245 const now = Date.now(); 14246 if (now - last < 300) { 14247 const src = btn.getAttribute('data-full') || ''; 14248 open(findIndexByUrl(absUrl(src))); 14249 last = 0; 14250 } else { 14251 last = now; 14252 } 14253 }); 14254 }); 14255 14256 // Ekspos API 14257 window.PDLightbox = { open, close, next, prev, findIndexByUrl }; 14258 } 14259 14260 if (document.readyState === 'loading') { 14261 document.addEventListener('DOMContentLoaded', initPdLightbox); 14262 } else { 14263 initPdLightbox(); 14264 } 14265})(); 14266</script>
vendor: 2 bytes, line 14266
14266
14267<script data-inlined="cart.js"> 14268(function(){ 14269 function $(s,ctx=document){ return ctx.querySelector(s); } 14270 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 14271 14272 // CSRF token helper 14273 function getCsrfToken() { 14274 const metaTag = document.querySelector('meta[name="csrf-token"]'); 14275 return metaTag ? metaTag.getAttribute('content') : ''; 14276 } 14277 14278 function guessPdMeta(){ 14279 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 14280 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 14281 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 14282 const imgEl = $('#pd-main-img'); 14283 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 14284 return { name, sku, image_url }; // price removed for security 14285 } 14286 14287 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 14288 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 14289 const payload = { 14290 product_id: parseInt(productId||0,10) || 0, 14291 qty: parseInt(qty||1,10) || 1, 14292 variant_id: parseInt(variantId||0,10) || 0, 14293 meta 14294 }; 14295 14296 // Check if user is logged in 14297 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 14298 document.body.classList.contains('logged-in') || 14299 window.userLoggedIn === true || 14300 document.body.dataset.userLoggedIn === 'true'; 14301 14302 // Skip localStorage untuk logged user - mereka pakai database 14303 if (!isLoggedIn && window.guestCart) { 14304 try { 14305 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 14306 } catch (e) { 14307 console.warn('Failed to add to localStorage cart:', e); 14308 } 14309 } 14310 14311 const res = await fetch('/api/cart/add.php', { 14312 method: 'POST', 14313 headers: { 14314 'Content-Type':'application/json', 14315 'X-CSRF-Token': getCsrfToken() 14316 }, 14317 credentials: 'same-origin', 14318 body: JSON.stringify({ 14319 ...payload, 14320 csrf_token: getCsrfToken() 14321 }), 14322 }); 14323 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 14324 if (!res.ok || !data.ok) { 14325 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 14326 throw new Error(msg); 14327 } 14328 14329 // Update badge di navbar dengan data dari response 14330 try { 14331 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14332 if (el && data.cart && typeof data.cart.items_count === 'number') { 14333 el.textContent = String(data.cart.items_count); 14334 el.classList.toggle('is-empty', data.cart.items_count <= 0); 14335 } 14336 } catch(_){} 14337 14338 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 14339 try { 14340 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 14341 } catch(_){} 14342 14343 // Update CartBadge dengan data dari response (lebih reliable) 14344 try { 14345 if (window.CartBadge && window.CartBadge.set) { 14346 window.CartBadge.set(data.cart.items_count||0); 14347 } 14348 } catch(_){} 14349 14350 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 14351 setTimeout(() => { 14352 if (window.updateCartCounter) { 14353 window.updateCartCounter(); 14354 } 14355 }, 100); 14356 14357 return data; 14358 } 14359 14360 // Function to update cart counter from server 14361 async function updateCartCounter(){ 14362 try { 14363 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 14364 const data = await res.json().catch(()=>({ok:false})); 14365 if (data.ok && typeof data.items_count === 'number') { 14366 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14367 if (el) { 14368 el.textContent = String(data.items_count); 14369 el.classList.toggle('is-empty', data.items_count <= 0); 14370 } 14371 // Dispatch event 14372 try { 14373 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 14374 } catch(_){} 14375 } 14376 } catch (err) { 14377 console.log('Could not update cart counter:', err); 14378 } 14379 } 14380 14381 // Function to manually merge guest cart (if needed) 14382 async function mergeGuestCart(){ 14383 try { 14384 const res = await fetch('/api/cart/merge-guest.php', { 14385 method: 'POST', 14386 headers: { 14387 'Content-Type': 'application/json', 14388 'X-CSRF-Token': getCsrfToken() 14389 }, 14390 credentials: 'same-origin', 14391 body: JSON.stringify({ csrf_token: getCsrfToken() }) 14392 }); 14393 const data = await res.json().catch(()=>({ok:false})); 14394 14395 if (data.ok && data.merged_items > 0) { 14396 console.log(`Guest cart merged: ${data.merged_items} items`); 14397 // Update cart counter after merge 14398 await updateCartCounter(); 14399 return data; 14400 } 14401 return data; 14402 } catch (err) { 14403 console.log('Could not merge guest cart:', err); 14404 return { ok: false, error: err.message }; 14405 } 14406 } 14407 14408 window.addToCart = addToCart; 14409 window.updateCartCounter = updateCartCounter; 14410 window.mergeGuestCart = mergeGuestCart; 14411 14412 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 14413 document.addEventListener('click', async (e)=>{ 14414 const btn = e.target.closest('[data-addtocart]'); 14415 if (!btn) return; 14416 e.preventDefault(); 14417 if (btn.disabled) return; 14418 14419 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 14420 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
14421 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 14422 14423 try { 14424 const data = await addToCart(pid, qty, vid); 14425 if (window.showToast) { 14426 const meta = guessPdMeta(); 14427 window.showToast({ 14428 type:'success', 14429 title:'Berhasil ditambahkan', 14430 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 14431 duration: 3800, 14432 actions: [ 14433 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 14434 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 14435 ] 14436 }); 14437 } 14438 } catch (err) { 14439 if (window.showToast) { 14440 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 14441 } else { 14442 console.error(err); 14443 } 14444 } 14445 }); 14446})(); 14447</script>
vendor: 2 bytes, line 14447
14447
14448<script data-inlined="stock-validator-vanilla.js"> 14449/** 14450 * Stock Validation - Vanilla JS Version 14451 * 14452 * This script provides client-side stock validation without jQuery dependency 14453 * - Works with native JavaScript and fetch API 14454 * - Graceful fallback for older browsers 14455 * - Same functionality as jQuery version 14456 */ 14457 14458window.StockValidator = (function() { 14459 'use strict'; 14460 14461 const config = { 14462 apiEndpoint: '/api/stock/check.php', 14463 modalSelector: '#stock-modal-overlay', // We create our own modal 14464 loadingClass: 'loading-stock', 14465 disabledClass: 'stock-disabled', 14466 debug: true // Enable debug logging 14467 }; 14468 14469 /** 14470 * Cross-browser event listener helper 14471 */ 14472 function addEventListener(element, event, handler) { 14473 if (element.addEventListener) { 14474 element.addEventListener(event, handler); 14475 } else if (element.attachEvent) { 14476 element.attachEvent('on' + event, handler); 14477 } 14478 } 14479 14480 /** 14481 * Get stock information from embedded variant data (instant, no AJAX needed) 14482 */ 14483 function getStockFromEmbeddedData(variantId) { 14484 // Check if embedded variant data exists 14485 if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { 14486 return null; 14487 } 14488 14489 const variants = window.__PD_VARIANTS_WITH_BRANCH__; 14490 14491 // SPECIAL CASE: Simple product (no variants) - empty array means simple product 14492 if (variants.length === 0) { 14493 // For simple products, we cannot determine stock from embedded data 14494 // Return null to force API validation 14495 return null; 14496 } 14497 14498 // If no variant specified, check base product stock (all variants) 14499 if (!variantId || variantId <= 0) { 14500 // For base product, check if ANY variant has stock 14501 const isPreorder = window.isPreorderProduct || false; 14502 let hasAnyStock = false; 14503 let totalStock = 0; 14504 14505 if (isPreorder) { 14506 // For pre-order, check preorder_remaining 14507 hasAnyStock = variants.some(v => { 14508 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14509 return v.preorder_remaining > 0; 14510 } 14511 return v.stock > 0; 14512 }); 14513 totalStock = variants.reduce((sum, v) => { 14514 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14515 return sum + (v.preorder_remaining || 0); 14516 } 14517 return sum + (v.stock || 0); 14518 }, 0); 14519 } else { 14520 hasAnyStock = variants.some(v => v.stock > 0); 14521 totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); 14522 } 14523 14524 return { 14525 hasStock: hasAnyStock, 14526 stock: totalStock, 14527 sku: 'Base Product', 14528 variant: null 14529 }; 14530 } 14531 14532 // Find specific variant 14533 const variant = variants.find(v => v.id === variantId); 14534 14535 if (!variant) { 14536 return { 14537 hasStock: false, 14538 stock: 0, 14539 sku: 'Unknown Variant', 14540 variant: null 14541 }; 14542 } 14543 14544 // For pre-order products, check preorder_remaining instead of stock 14545 const isPreorder = window.isPreorderProduct || false; 14546 let hasStock = false; 14547 let availableStock = 0; 14548 14549 if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { 14550 availableStock = parseInt(variant.preorder_remaining, 10); 14551 if (isNaN(availableStock)) availableStock = 0; 14552 hasStock = availableStock > 0; 14553 } else { 14554 availableStock = variant.stock || 0; 14555 hasStock = availableStock > 0; 14556 } 14557 14558 return { 14559 hasStock: hasStock, 14560 stock: availableStock, 14561 sku: variant.sku, 14562 variant: variant 14563 }; 14564 } 14565 14566 /** 14567 * Cross-browser AJAX helper 14568 */
14569 function makeRequest(url, options) { 14570 return new Promise((resolve, reject) => { 14571 // Try fetch first (modern browsers) 14572 if (typeof fetch !== 'undefined') { 14573 const fetchOptions = { 14574 method: options.method || 'POST', 14575 headers: { 14576 'Content-Type': 'application/x-www-form-urlencoded', 14577 }, 14578 body: options.data ? new URLSearchParams(options.data).toString() : null 14579 }; 14580 14581 fetch(url, fetchOptions) 14582 .then(response => { 14583 if (!response.ok) { 14584 throw new Error(`HTTP ${response.status}`); 14585 } 14586 return response.json(); 14587 }) 14588 .then(resolve) 14589 .catch(reject); 14590 } else { 14591 // Fallback to XMLHttpRequest 14592 const xhr = new XMLHttpRequest(); 14593 xhr.open(options.method || 'POST', url); 14594 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 14595 14596 xhr.onload = function() { 14597 if (xhr.status >= 200 && xhr.status < 300) { 14598 try { 14599 resolve(JSON.parse(xhr.responseText)); 14600 } catch (e) { 14601 reject(new Error('Invalid JSON response')); 14602 } 14603 } else { 14604 reject(new Error(`HTTP ${xhr.status}`)); 14605 } 14606 }; 14607 14608 xhr.onerror = function() { 14609 reject(new Error('Network error')); 14610 }; 14611 14612 const data = options.data ? new URLSearchParams(options.data).toString() : null; 14613 xhr.send(data); 14614 } 14615 }); 14616 } 14617 14618 /** 14619 * Check stock via AJAX 14620 */ 14621 function checkStock(productId, variantId, qty) { 14622 return makeRequest(config.apiEndpoint, { 14623 method: 'POST', 14624 data: { 14625 product_id: productId, 14626 variant_id: variantId || 0, 14627 qty: qty || 1 14628 } 14629 }); 14630 } 14631 14632 /** 14633 * Create and show stock alert modal using modal.css structure 14634 */ 14635 function showStockModal(stockInfo, action = 'add to cart') { 14636 let title, message, type, iconClass; 14637 14638 // Handle both string and object formats 14639 if (typeof stockInfo === 'string') { 14640 title = 'Stok Tidak Tersedia'; 14641 message = stockInfo; 14642 type = 'warning'; 14643 iconClass = 'warning'; 14644 } else if (stockInfo && typeof stockInfo === 'object') { 14645 if (!stockInfo.is_active) { 14646 title = 'Produk Tidak Tersedia'; 14647 message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; 14648 type = 'danger'; 14649 iconClass = 'danger'; 14650 } else if (!stockInfo.is_in_stock) { 14651 title = 'Stok Habis'; 14652 message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; 14653 if (stockInfo.sku) { 14654 message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`; 14655 } 14656 type = 'warning'; 14657 iconClass = 'warning'; 14658 } else if (!stockInfo.can_add_to_cart) { 14659 title = 'Stok Tidak Mencukupi'; 14660 message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`; 14661 message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`; 14662 message += `Silakan kurangi jumlah atau pilih varian lain.`; 14663 type = 'warning'; 14664 iconClass = 'warning'; 14665 } else { 14666 title = 'Stok Tidak Tersedia'; 14667 message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14668 type = 'warning'; 14669 iconClass = 'warning'; 14670 } 14671 } else { 14672 title = 'Stok Tidak Tersedia'; 14673 message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14674 type = 'warning'; 14675 iconClass = 'warning'; 14676 } 14677 14678 // Create modal HTML using modal.css structure 14679 const modalHTML = ` 14680 <div class="modal-overlay" id="stock-modal-overlay"> 14681 <div class="modal-dialog"> 14682 <div class="modal-header"> 14683 <div class="modal-icon ${iconClass}"> 14684 ${getIconSVG(iconClass)} 14685 </div> 14686 <h3 class="modal-title">${title}</h3> 14687 </div> 14688 <div class="modal-body"> 14689 <p class="modal-message">${message}</p> 14690 <div class="modal-actions"> 14691 <button type="button" class="modal-btn modal-btn-secondary" data-action="close"> 14692 Tutup 14693 </button> 14694 <button type="button" class="modal-btn modal-btn-primary" data-action="reload"> 14695 Muat Ulang 14696 </button> 14697 </div> 14698 </div> 14699 </div> 14700 </div> 14701 `; 14702 14703 // Remove existing modal if any 14704 const existingModal = document.getElementById('stock-modal-overlay'); 14705 if (existingModal) { 14706 existingModal.remove(); 14707 } 14708 14709 // Add modal to body 14710 document.body.insertAdjacentHTML('beforeend', modalHTML); 14711 14712 // Show modal with animation 14713 const modal = document.getElementById('stock-modal-overlay'); 14714 setTimeout(() => { 14715 modal.classList.add('show'); 14716 }, 10); 14717 14718 // Add event listeners to modal buttons 14719 const closeBtn = modal.querySelector('[data-action="close"]'); 14720 const reloadBtn = modal.querySelector('[data-action="reload"]'); 14721 14722 if (closeBtn) { 14723 addEventListener(closeBtn, 'click', closeStockModal); 14724 } 14725 14726 if (reloadBtn) { 14727 addEventListener(reloadBtn, 'click', function() { 14728 closeStockModal(); 14729 window.location.reload(); 14730 }); 14731 } 14732 14733 // Close modal when clicking overlay 14734 addEventListener(modal, 'click', function(event) { 14735 if (event.target === modal) { 14736 closeStockModal(); 14737 } 14738 }); 14739 14740 // Auto-close after 8 seconds 14741 setTimeout(() => { 14742 closeStockModal(); 14743 }, 8000); 14744 } 14745 14746 /** 14747 * Get icon SVG based on type 14748 */ 14749 function getIconSVG(type) { 14750 const icons = { 14751 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>', 14752 warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
14752<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>', 14753 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>', 14754 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>' 14755 }; 14756 return icons[type] || icons.info; 14757 } 14758 14759 /** 14760 * Close stock modal 14761 */ 14762 function closeStockModal() { 14763 const modal = document.getElementById('stock-modal-overlay'); 14764 if (modal) { 14765 modal.classList.remove('show'); 14766 setTimeout(() => { 14767 modal.remove(); 14768 }, 200); 14769 } 14770 } 14771 14772 // Make closeStockModal globally available 14773 if (typeof window !== 'undefined') { 14774 window.closeStockModal = closeStockModal; 14775 } 14776 14777 /** 14778 * Get element data attribute with fallback 14779 */ 14780 function getDataAttribute(element, attr) { 14781 if (element.dataset) { 14782 return element.dataset[attr]; 14783 } else { 14784 return element.getAttribute('data-' + attr); 14785 } 14786 } 14787 14788 /** 14789 * Validate stock before cart action 14790 */ 14791 async function validateBeforeCart(productId, variantId, qty, action = 'add') { 14792 try { 14793 // SMART VALIDATION: For simple products, check if we already have cached stock info 14794 // If buttons are enabled and cached info shows stock available, skip API validation 14795 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { 14796 14797 return true; 14798 } 14799 14800 // For variant products or when cached info is not available, do API validation 14801 const stockInfo = await checkStock(productId, variantId, qty); 14802 14803 if (!stockInfo.ok) { 14804 showStockModal({ 14805 message: stockInfo.error || 'Gagal mengecek stok', 14806 is_active: false 14807 }, action); 14808 return false; 14809 } 14810 14811 if (!stockInfo.can_add_to_cart) { 14812 showStockModal(stockInfo, action); 14813 return false; 14814 } 14815 14816 return true; // Stock available, proceed 14817 14818 } catch (error) { 14819 console.error('Stock validation error:', error); 14820 showStockModal({ 14821 message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', 14822 is_active: false 14823 }, action); 14824 return false; 14825 } 14826 } 14827 14828 /** 14829 * Handle button click with stock validation 14830 */ 14831 function handleButtonClick(event, action) { 14832 const button = event.target; 14833 14834 14835 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14836 if (button.hasAttribute('data-stock-blocked')) { 14837 event.preventDefault(); 14838 event.stopImmediatePropagation(); 14839 14840 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14841 showStockModal(reason); 14842 14843 // Show security warning 14844 if (window.showToast) { 14845 window.showToast({ 14846 type: 'warning', 14847 title: 'Aksi Tidak Diizinkan', 14848 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14849 duration: 3000 14850 }); 14851 } 14852 return false; 14853 } 14854 14855 // SECOND CHECK: Real-time stock validation against cached stock info 14856 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14857 event.preventDefault(); 14858 event.stopImmediatePropagation(); 14859 14860 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14861 14862 // Re-enforce button disabling 14863 button.disabled = true; 14864 button.style.opacity = '0.5'; 14865 button.style.cursor = 'not-allowed'; 14866 button.setAttribute('data-stock-blocked', 'true'); 14867 14868 return false; 14869 } 14870 14871 // Skip if already processing 14872 if (button.classList.contains(config.loadingClass)) { 14873 event.preventDefault(); 14874 return false; 14875 } 14876 14877 // Get product data from button attributes or form 14878 let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); 14879 let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); 14880 let qty = parseInt(getDataAttribute(button, 'qty') || 1); 14881 14882 // Try to get from global variables (set by product detail page) 14883 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14884 productId = parseInt(window.PD_ID || 0); 14885 } 14886 14887 // Try to get from parent form if not in button 14888 if (productId <= 0) { 14889 const form = button.closest ? button.closest('form') : null; 14890 if (form) { 14891 const pidAttr = form.getAttribute('data-pid'); 14892 const pidInput = form.querySelector('[name="product_id"]'); 14893 const vidInput = form.querySelector('[name="variant_id"]');
14894 const qtyInput = form.querySelector('[name="qty"]'); 14895 14896 if (pidAttr) productId = parseInt(pidAttr); 14897 if (pidInput) productId = parseInt(pidInput.value || 0); 14898 if (vidInput) variantId = parseInt(vidInput.value || 0); 14899 if (qtyInput) qty = parseInt(qtyInput.value || 1); 14900 } 14901 } 14902 14903 // CRITICAL: Always get current variant from selector (overrides button attributes) 14904 // This ensures we validate against the currently selected variant, not cached button data 14905 const variantSelector = document.getElementById('pd-variant-id'); 14906 if (variantSelector) { 14907 const currentVariantId = parseInt(variantSelector.value || 0); 14908 if (currentVariantId > 0) { 14909 variantId = currentVariantId; 14910 } 14911 } 14912 14913 14914 if (productId <= 0) { 14915 return true; // Let default handler deal with it 14916 } 14917 14918 // Prevent default and validate stock 14919 event.preventDefault(); 14920 event.stopPropagation(); 14921 14922 // Show loading state 14923 button.classList.add(config.loadingClass); 14924 const originalDisabled = button.disabled; 14925 button.disabled = true; 14926 14927 14928 validateBeforeCart(productId, variantId, qty, action) 14929 .then(isValid => { 14930 if (isValid) { 14931 // Stock OK, proceed with original action 14932 if (action === 'cart' && typeof window.addToCart === 'function') { 14933 window.addToCart(productId, qty, variantId); 14934 } else if (action === 'buy' && typeof window.buyNow === 'function') { 14935 // Call buyNow from pd-buynow.js without parameters (it gets data internally) 14936 window.buyNow(); 14937 } else { 14938 // Add fallback cart/buy logic here if needed 14939 } 14940 } 14941 }) 14942 .catch(error => { 14943 }) 14944 .finally(() => { 14945 // Remove loading state 14946 button.classList.remove(config.loadingClass); 14947 button.disabled = originalDisabled; 14948 }); 14949 14950 return false; 14951 } 14952 14953 /** 14954 * Handle mobile button click with stock validation 14955 */ 14956 function handleMobileButtonClick(event, action) { 14957 const button = event.target; 14958 14959 14960 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14961 if (button.hasAttribute('data-stock-blocked')) { 14962 event.preventDefault(); 14963 event.stopImmediatePropagation(); 14964 14965 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14966 showStockModal(reason); 14967 14968 // Show security warning 14969 if (window.showToast) { 14970 window.showToast({ 14971 type: 'warning', 14972 title: 'Aksi Tidak Diizinkan', 14973 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14974 duration: 3000 14975 }); 14976 } 14977 return false; 14978 } 14979 14980 // SECOND CHECK: Real-time stock validation against cached stock info 14981 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14982 event.preventDefault(); 14983 event.stopImmediatePropagation(); 14984 14985 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14986 14987 // Re-enforce button disabling 14988 button.disabled = true; 14989 button.style.opacity = '0.5'; 14990 button.style.cursor = 'not-allowed'; 14991 button.setAttribute('data-stock-blocked', 'true'); 14992 14993 return false; 14994 } 14995 14996 // Skip if already processing 14997 if (button.classList.contains(config.loadingClass)) { 14998 event.preventDefault(); 14999 return false; 15000 } 15001 15002 // Get product data from mobile FAB container 15003 let productId = 0; 15004 let variantId = 0; 15005 let qty = 1; 15006
15007 // Try to get from mobile FAB data-pid 15008 const mobileFab = document.getElementById('pd-mobile-fab'); 15009 if (mobileFab) { 15010 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15011 if (pidAttr) productId = parseInt(pidAttr); 15012 } 15013 15014 // Try to get from global variables (set by product detail page) 15015 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 15016 productId = parseInt(window.PD_ID || 0); 15017 } 15018 15019 // CRITICAL: Always get current variant from selector (real-time) 15020 // This ensures we validate against the currently selected variant 15021 const variantSelector = document.getElementById('pd-variant-id'); 15022 if (variantSelector) { 15023 variantId = parseInt(variantSelector.value || 0); 15024 } 15025 15026 // Try to get quantity from quantity input 15027 const qtySelector = document.querySelector('[name="qty"]'); 15028 if (qtySelector) { 15029 qty = parseInt(qtySelector.value || 1); 15030 } 15031 15032 // Ensure minimum quantity is 1 15033 qty = Math.max(1, qty); 15034 15035 15036 if (productId <= 0) { 15037 return true; // Let default handler deal with it 15038 } 15039 15040 // Event already prevented at interceptor level, just proceed with validation 15041 15042 // Show loading state 15043 button.classList.add(config.loadingClass); 15044 const originalDisabled = button.disabled; 15045 const originalText = button.textContent; 15046 button.disabled = true; 15047 button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; 15048 15049 15050 // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions 15051 if (window.addToCart && typeof window.addToCart === 'function') { 15052 window.addToCart._stockValidatorBlocked = true; 15053 } 15054 15055 // Also preemptively block other cart functions 15056 const originalAddToCartSubmit = window.addToCartSubmit; 15057 const originalPdAddToCartSubmit = window._pdAddToCartSubmit; 15058 15059 // Temporarily block ALL cart functions during validation 15060 window.addToCartSubmit = (...args) => { 15061 return Promise.resolve(false); 15062 }; 15063 15064 if (originalPdAddToCartSubmit) { 15065 window._pdAddToCartSubmit = (...args) => { 15066 return Promise.resolve(false); 15067 }; 15068 } 15069 15070 validateBeforeCart(productId, variantId, qty, action) 15071 .then(isValid => { 15072 if (isValid) { 15073 // Stock OK, proceed with original action 15074 15075 if (action === 'cart') { 15076 // For mobile cart, we need to recreate the original behavior 15077 // Strategy: Try multiple methods to ensure cart add + toast success 15078 15079 15080 let cartAddSuccess = false; 15081 let errorMessage = ''; 15082 15083 // Method 1: Try original addToCart function 15084 const originalAddToCart = window.addToCart && window.addToCart._original; 15085 if (originalAddToCart && !cartAddSuccess) { 15086 try { 15087 const result = originalAddToCart(productId, qty, variantId); 15088 15089 // Handle both sync and async returns 15090 if (result && typeof result.then === 'function') { 15091 result.then(() => { 15092 cartAddSuccess = true; 15093 }).catch(err => { 15094 errorMessage = err.message || 'addToCart failed'; 15095 }); 15096 } else { 15097 cartAddSuccess = true; 15098 } 15099 } catch (error) { 15100 errorMessage = error.message || 'addToCart exception'; 15101 } 15102 } 15103 15104 // Method 2: Try pd-addtocart.js function (backup method) 15105 if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { 15106 try { 15107 15108 // Create fake form for pd-addtocart.js 15109 const fakeForm = document.createElement('form');
15110 const qtyInput = document.createElement('input'); 15111 qtyInput.name = 'qty'; 15112 qtyInput.value = qty; 15113 fakeForm.appendChild(qtyInput); 15114 15115 // Set variant selector value for pd-addtocart to read 15116 const variantSelector = document.getElementById('pd-variant-id'); 15117 if (variantSelector) { 15118 variantSelector.value = variantId; 15119 } 15120 15121 const fakeEvent = { 15122 preventDefault: () => {}, 15123 target: fakeForm 15124 }; 15125 15126 // Call pd-addtocart.js function (this should handle toast internally) 15127 const result = window._pdAddToCartSubmit(fakeEvent, productId); 15128 if (result && typeof result.then === 'function') { 15129 result.then(() => { 15130 cartAddSuccess = true; 15131 }); 15132 } else { 15133 cartAddSuccess = true; 15134 } 15135 } catch (error) { 15136 } 15137 } 15138 15139 // Method 3: Direct addToCart call (fallback) 15140 if (typeof window.addToCart === 'function' && !cartAddSuccess) { 15141 try { 15142 const result = window.addToCart(productId, qty, variantId); 15143 if (result && typeof result.then === 'function') { 15144 result.then(() => { 15145 cartAddSuccess = true; 15146 }); 15147 } else { 15148 cartAddSuccess = true; 15149 } 15150 } catch (error) { 15151 } 15152 } 15153 15154 // Always show success toast for mobile (regardless of which method worked) 15155 // This ensures user gets feedback for successful cart addition 15156 setTimeout(() => { 15157 if (window.showToast) { 15158 15159 // Get product details for toast 15160 const getVariantSummaryText = () => { 15161 const vs = document.getElementById('pd-variant-summary'); 15162 if (!vs) return ''; 15163 const items = Array.from(vs.querySelectorAll('.vs-item strong')) 15164 .map(el => el.textContent.trim()) 15165 .filter(Boolean); 15166 return items.length ? items.join(' / ') : ''; 15167 }; 15168 15169 const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ 15170 '&':'&','<':'<','>':'>','"':'"',"'":''' 15171 }[m])); 15172 15173 const variantText = getVariantSummaryText(); 15174 const skuText = (function(){ 15175 const el = document.getElementById('pd-sku'); 15176 return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 15177 })(); 15178 const productName = window.PD_NAME || 'Product'; 15179 15180 window.showToast({ 15181 type: 'success', 15182 title: 'Berhasil ditambahkan ke Keranjang', 15183 message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` ÷ <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
15183 ÷ Qty: ${qty}`, 15184 duration: 5000, 15185 actions: [ 15186 { 15187 label: 'Lihat Keranjang', 15188 onClick: () => { window.location.href = '/keranjang'; } 15189 }, 15190 { 15191 label: 'Lanjut Belanja', 15192 variant: 'alt', 15193 onClick: () => {} 15194 } 15195 ] 15196 }); 15197 } else { 15198 } 15199 }, 100); // Small delay to ensure cart operation completes first 15200 15201 } else if (action === 'buy') { 15202 // For mobile buy, call buyNow 15203 if (typeof window.buyNow === 'function') { 15204 window.buyNow(); 15205 } 15206 } 15207 } else { 15208 // Stock validation FAILED - absolutely NO cart function calls allowed 15209 15210 // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) 15211 } 15212 15213 // Always restore functions after validation (success or failure) 15214 setTimeout(() => { 15215 // Remove blocking flag 15216 if (window.addToCart && typeof window.addToCart === 'function') { 15217 window.addToCart._stockValidatorBlocked = false; 15218 } 15219 15220 // Restore other functions 15221 if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { 15222 window.addToCartSubmit = originalAddToCartSubmit; 15223 } 15224 if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { 15225 window._pdAddToCartSubmit = originalPdAddToCartSubmit; 15226 } 15227 15228 }, 1000); // Shorter timeout since we want to restore quickly after success 15229 }) 15230 .catch(error => { 15231 // On error, also block the action 15232 }) 15233 .finally(() => { 15234 // Remove loading state 15235 button.classList.remove(config.loadingClass); 15236 button.disabled = originalDisabled; 15237 button.textContent = originalText; 15238 }); 15239 15240 return false; 15241 } 15242 15243 /** 15244 * Intercept cart buttons 15245 */ 15246 function interceptCartButtons() { 15247 15248 15249 // CRITICAL: Remove existing mobile cart handlers from product-detail.php 15250 setTimeout(() => { 15251 const mobileCartBtn = document.getElementById('mf-cart'); 15252 if (mobileCartBtn) { 15253 // Clone the button to remove ALL existing event listeners 15254 const newBtn = mobileCartBtn.cloneNode(true); 15255 mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); 15256 15257 15258 // Add our own handler to the new button 15259 newBtn.addEventListener('click', function(event) { 15260 event.preventDefault(); 15261 event.stopPropagation(); 15262 event.stopImmediatePropagation(); 15263 handleMobileButtonClick(event, 'cart'); 15264 return false; 15265 }, true); 15266 } 15267 }, 50); 15268 15269 // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) 15270 // This provides better integration with existing pd-addtocart.js toast system 15271 15272 // Buy now buttons - updated selectors (desktop + mobile) 15273 addEventListener(document, 'click', function(event) { 15274 const target = event.target; 15275 15276 // Desktop buy buttons 15277 if (target.classList.contains('btn-buy') || // Product detail buy button 15278 target.classList.contains('btn-buy-now') || 15279 target.classList.contains('buy-now-btn') || 15280 target.id === 'btn-buy' || // Specific ID match 15281 getDataAttribute(target, 'action') === 'buy-now' || 15282 getDataAttribute(target, 'action') === 'buy') { 15283 15284 handleButtonClick(event, 'buy'); 15285 } 15286 15287 // Mobile buy buttons 15288 else if (target.classList.contains('mf-buy') || // Mobile fab buy button 15289 target.id === 'mf-buy') { // Mobile buy button ID 15290 15291 handleMobileButtonClick(event, 'buy'); 15292 } 15293 15294 // Mobile cart buttons - ENHANCED BLOCKING 15295 else if (target.classList.contains('mf-cart') || // Mobile fab cart button 15296 target.id === 'mf-cart') { // Mobile cart button ID 15297 15298 15299 // Immediately prevent all event propagation 15300 event.preventDefault(); 15301 event.stopPropagation(); 15302 event.stopImmediatePropagation(); 15303 15304 handleMobileButtonClick(event, 'cart'); 15305 15306 // Return false to ensure no other handlers run 15307 return false; 15308 } 15309 }, true);
15309 // Use capturing phase to ensure we intercept before other handlers 15310 15311 // Additional protection: Override addToCart function to add mobile button state checking 15312 const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; 15313 if (originalAddToCart) { 15314 window.addToCart = async function(...args) { 15315 // Check if this call is coming from a mobile button that's currently being validated 15316 const mobileCartBtn = document.getElementById('mf-cart'); 15317 if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { 15318 return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); 15319 } 15320 15321 // Check if functions are temporarily blocked (during stock failure) 15322 if (window.addToCart._stockValidatorBlocked) { 15323 return Promise.reject(new Error('Stock validation failed - cart action blocked')); 15324 } 15325 15326 // Allow normal calls 15327 return originalAddToCart.apply(this, args); 15328 }; 15329 15330 // Store reference to original for restoration 15331 window.addToCart._original = originalAddToCart; 15332 } 15333 } 15334 15335 /** 15336 * Setup global click interceptor for security 15337 * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler 15338 */ 15339 function setupGlobalClickInterceptor() { 15340 // Add capture-phase listener to catch clicks before other handlers 15341 document.addEventListener('click', function(event) { 15342 const target = event.target; 15343 15344 // Check if this is a cart/buy button 15345 const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || 15346 target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); 15347 const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || 15348 target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); 15349 15350 if (!isCartButton && !isBuyButton) { 15351 return; // Not our target, allow normal processing 15352 } 15353 15354 const button = isCartButton ? 15355 (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')) : 15356 (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); 15357 15358 // IMMEDIATE BLOCK: Prevent any processing first 15359 event.preventDefault(); 15360 event.stopImmediatePropagation(); 15361 15362 // SECURITY CHECK 1: Is button marked as stock-blocked? 15363 if (button && button.hasAttribute('data-stock-blocked')) { 15364 15365 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 15366 showStockModal(reason); 15367 15368 // Show security alert 15369 if (window.showToast) { 15370 window.showToast({ 15371 type: 'error', 15372 title: 'Aksi Diblokir', 15373 message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', 15374 duration: 4000 15375 }); 15376 } 15377 15378 return false; 15379 } 15380 15381 // SECURITY CHECK 2: Real-time validation against cached stock info 15382 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 15383 15384 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 15385 15386 // Re-enforce button disabling 15387 if (button) { 15388 button.disabled = true; 15389 button.style.opacity = '0.5'; 15390 button.style.cursor = 'not-allowed'; 15391 button.setAttribute('data-stock-blocked', 'true'); 15392 button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); 15393 } 15394 15395 return false; 15396 } 15397 15398 // SECURITY CHECK 3: Smart client-side validation 15399 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15400 const variantSelector = document.getElementById('pd-variant-id'); 15401 const variantId = variantSelector ? parseInt(variantSelector.value || 0
15401) : 0; 15402 15403 if (productId > 0) { 15404 // SMART VALIDATION: If we already have cached stock info showing stock is available, 15405 // and buttons are enabled, proceed immediately without re-validation 15406 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 15407 button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15408 15409 // Allow the action to proceed immediately with toast 15410 if (isCartButton) { 15411 // Trigger cart action with proper toast handling 15412 if (typeof window._pdAddToCartSubmit === 'function') { 15413 // Use pd-addtocart.js function for proper toast handling 15414 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15415 if (form) { 15416 const fakeEvent = { 15417 target: form, 15418 preventDefault: () => {} 15419 }; 15420 window._pdAddToCartSubmit(fakeEvent, productId); 15421 } 15422 } else if (typeof window.addToCartSubmit === 'function') { 15423 // Fallback to addToCartSubmit 15424 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15425 if (form) { 15426 const fakeEvent = { 15427 target: form, 15428 preventDefault: () => {} 15429 }; 15430 window.addToCartSubmit(fakeEvent, productId); 15431 } 15432 } else if (typeof window.addToCart === 'function') { 15433 // Direct addToCart call 15434 window.addToCart(productId, 1, variantId); 15435 } 15436 } else if (isBuyButton) { 15437 // Trigger buy action 15438 if (typeof window.buyNow === 'function') { 15439 window.buyNow(); 15440 } 15441 } 15442 15443 return false; // Action completed 15444 } 15445 15446 // Get stock info from embedded variant data (for variant products) 15447 const stockInfo = getStockFromEmbeddedData(variantId); 15448 15449 if (stockInfo && stockInfo.hasStock) { 15450 15451 // Update global stock cache 15452 window._currentStockInfo = { 15453 can_add_to_cart: true, 15454 is_in_stock: true, 15455 available_stock: stockInfo.stock, 15456 message: 'Stok tersedia' 15457 }; 15458 15459 // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) 15460 // If data-stock-blocked is set, respect it and don't enable the button 15461 const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; 15462 15463 if (!isExplicitlyBlocked) { 15464 // Remove stock blocking if it was set and enable button 15465 button.removeAttribute('data-stock-blocked'); 15466 button.removeAttribute('data-stock-reason'); 15467 button.disabled = false; 15468 button.style.opacity = ''; 15469 button.style.cursor = ''; 15470 button.classList.remove(config.disabledClass); 15471 } else { 15472 // Button is blocked by variant logic, show reason 15473 const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; 15474 console.log('Button blocked by variant logic:', reason); 15475 e.preventDefault(); 15476 e.stopPropagation(); 15477 return false; 15478 } 15479 15480 // Allow the action to proceed immediately with toast 15481 if (isCartButton) { 15482 // Trigger cart action with proper toast handling 15483 if (typeof window._pdAddToCartSubmit === 'function') { 15484 // Use pd-addtocart.js function for proper toast handling 15485 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15486 if (form) { 15487 const fakeEvent = { 15488 target: form, 15489 preventDefault: () => {} 15490 }; 15491 window._pdAddToCartSubmit(fakeEvent, productId); 15492 } 15493 } else if (typeof window.addToCartSubmit === 'function') { 15494 // Fallback to addToCartSubmit 15495 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15496 if (form) { 15497 const fakeEvent = { 15498 target: form, 15499 preventDefault: () => {} 15500 }; 15501 window.addToCartSubmit(fakeEvent, productId); 15502 } 15503 } else if (typeof window.addToCart === 'function') { 15504 // Direct addToCart call 15505 window.addToCart(productId, 1, variantId); 15506 } 15507 } else if (isBuyButton) { 15508 // Trigger buy action 15509 if (typeof window.buyNow === 'function') { 15510 window.buyNow(); 15511 } 15512 } 15513 } else if (stockInfo && !stockInfo.hasStock) { 15514 15515 // Update global stock cache 15516 window._currentStockInfo = { 15517 can_add_to_cart: false, 15518 is_in_stock: false, 15519 available_stock: stockInfo.stock, 15520 message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` 15521 }; 15522 15523 // Force block button 15524 button.disabled = true; 15525 button.style.opacity = '0.5'; 15526 button.style.cursor = 'not-allowed'; 15527 button.setAttribute('data-stock-blocked', 'true'); 15528 button.setAttribute('data-stock-reason', window._currentStockInfo.message); 15529 15530 // Show modal with variant info 15531 const modalMessage = stockInfo ? 15532 `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 15533 'Produk ini sedang habis stok. Silakan pilih varian lain.'; 15534 15535 showStockModal(modalMessage); 15536 15537 // Show security warning 15538 if (window.showToast) { 15539 window.showToast({ 15540 type: 'warning',
15541 title: 'Stok Tidak Tersedia', 15542 message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', 15543 duration: 3000 15544 }); 15545 } 15546 } else { 15547 // stockInfo is null (simple product) - this means embedded data is not available 15548 // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) 15549 if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15550 15551 // Allow the action to proceed immediately with toast 15552 if (isCartButton) { 15553 // Trigger cart action with proper toast handling 15554 if (typeof window._pdAddToCartSubmit === 'function') { 15555 // Use pd-addtocart.js function for proper toast handling 15556 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15557 if (form) { 15558 const fakeEvent = { 15559 target: form, 15560 preventDefault: () => {} 15561 }; 15562 window._pdAddToCartSubmit(fakeEvent, productId); 15563 } 15564 } else if (typeof window.addToCartSubmit === 'function') { 15565 // Fallback to addToCartSubmit 15566 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15567 if (form) { 15568 const fakeEvent = { 15569 target: form, 15570 preventDefault: () => {} 15571 }; 15572 window.addToCartSubmit(fakeEvent, productId); 15573 } 15574 } else if (typeof window.addToCart === 'function') { 15575 // Direct addToCart call 15576 window.addToCart(productId, 1, variantId); 15577 } 15578 } else if (isBuyButton) { 15579 // Trigger buy action 15580 if (typeof window.buyNow === 'function') { 15581 window.buyNow(); 15582 } 15583 } 15584 } else { 15585 // Button is disabled, don't allow action 15586 showStockModal('Produk ini sedang tidak tersedia.'); 15587 } 15588 } 15589 } else { 15590 // No product ID, block action 15591 showStockModal('Data produk tidak valid.'); 15592 } 15593 15594 return false; // Always block at capture phase 15595 15596 }, true); // Use capture phase to run BEFORE other listeners 15597 15598 15599 } 15600 15601 /** 15602 * Initialize button data attributes from current page 15603 */ 15604 function initializeButtonAttributes() { 15605 // Get product data from page globals or form 15606 let productId = 0, variantId = 0; 15607 15608 // Try to get from global variables (set by PHP) 15609 if (window.PD_ID) productId = parseInt(window.PD_ID); 15610 15611 // Try to get from form 15612 const form = document.getElementById('pd-buy-form'); 15613 if (form) { 15614 const pidAttr = form.getAttribute('data-pid'); 15615 if (pidAttr) productId = parseInt(pidAttr); 15616 15617 const varInput = document.getElementById('pd-variant-id'); 15618 if (varInput) variantId = parseInt(varInput.value || 0); 15619 } 15620 15621 // Try to get from mobile FAB 15622 const mobileFab = document.getElementById('pd-mobile-fab'); 15623 if (mobileFab && productId <= 0) { 15624 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15625 if (pidAttr) productId = parseInt(pidAttr); 15626 } 15627 15628 // Set attributes on desktop buttons 15629 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15630 desktopButtons.forEach(button => { 15631 if (productId > 0) { 15632 button.setAttribute('data-product-id', productId); 15633 if (variantId > 0) { 15634 button.setAttribute('data-variant-id', variantId); 15635 } 15636 } 15637 }); 15638 15639 // Set attributes on mobile buttons 15640 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15641 mobileButtons.forEach(button => { 15642 if (productId > 0) { 15643 button.setAttribute('data-product-id', productId); 15644 if (variantId > 0) { 15645 button.setAttribute('data-variant-id', variantId); 15646 } 15647 } 15648 }); 15649 15650 15651 15652 // Initial stock display update on page load 15653 if (productId > 0) { 15654 setTimeout(() => { 15655 updateStockDisplay(productId, variantId); 15656 15657 }, 100); 15658 } 15659 } 15660 15661 /** 15662 * Update button attributes when variant changes 15663 */ 15664 function updateButtonAttributesForVariant(variantId) { 15665 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15666 15667 if (productId <= 0) return; 15668 15669 // Update desktop buttons 15670 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15671 desktopButtons.forEach(button => { 15672 button.setAttribute('data-product-id', productId); 15673 if (variantId > 0) { 15674 button.setAttribute('data-variant-id', variantId); 15675 } else { 15676 button.removeAttribute('data-variant-id'); 15677 } 15678 }); 15679 15680 // Update mobile buttons 15681 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15682 mobileButtons.forEach(button => { 15683 button.setAttribute('data-product-id', productId); 15684 if (variantId > 0) { 15685 button.setAttribute('data-variant-id', variantId); 15686 } else { 15687 button.removeAttribute('data-variant-id'); 15688 } 15689 }); 15690 15691 15692 } 15693 15694 /** 15695 * Setup variant change integration with existing product-detail.php handlers 15696 */ 15697 function setupVariantChangeIntegration() { 15698 // Strategy: Hook into existing applyVariant function instead of adding new listener 15699 // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) 15700 15701 // Method 1: Hook into existing applyVariant function 15702 setTimeout(() => { 15703 if (typeof window.applyVariant === 'function') { 15704 const originalApplyVariant = window.applyVariant; 15705 window.applyVariant = function(v, focusImage) { 15706 // Call original function first 15707 const result = originalApplyVariant.call(this, v, focusImage); 15708 15709 // Then add our stock validator logic 15710 const variantId = v ? (v.id || v.ID || 0) : 0; 15711 15712 // Update button attributes for stock validator 15713 updateButtonAttributesForVariant(variantId); 15714 15715 // Update stock display using embedded data (instant) 15716 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15717 if (productId > 0) { 15718 updateStockDisplay(productId, variantId); 15719 } return result; 15720 }; 15721 15722 15723 } else { 15724 15725 setupVariantObserver(); 15726 } 15727 }, 500); 15728 15729 // Method 2: Direct observation of hidden input value changes (fallback) 15730 setTimeout(() => { 15731 setupVariantObserver(); 15732 }, 1000); 15733 } 15734 15735 /** 15736 * Setup variant observer as fallback when applyVariant hook fail
15736s 15737 */ 15738 function setupVariantObserver() { 15739 const variantInput = document.getElementById('pd-variant-id'); 15740 if (variantInput) { 15741 let lastVariantId = parseInt(variantInput.value || '0'); 15742 15743 // Use MutationObserver to watch for value changes 15744 if (window.MutationObserver) { 15745 const observer = new MutationObserver((mutations) => { 15746 mutations.forEach((mutation) => { 15747 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 15748 const newVariantId = parseInt(variantInput.value || '0'); 15749 if (newVariantId !== lastVariantId) { 15750 15751 updateButtonAttributesForVariant(newVariantId); 15752 15753 // Update stock display with embedded data 15754 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15755 if (productId > 0) { 15756 updateStockDisplay(productId, newVariantId); 15757 } 15758 15759 lastVariantId = newVariantId; 15760 } 15761 } 15762 }); 15763 }); 15764 15765 observer.observe(variantInput, { 15766 attributes: true, 15767 attributeFilter: ['value'] 15768 }); 15769 15770 15771 } 15772 15773 // Additional polling as ultimate fallback 15774 setInterval(() => { 15775 const currentVariantId = parseInt(variantInput.value || '0'); 15776 if (currentVariantId !== lastVariantId) { 15777 15778 updateButtonAttributesForVariant(currentVariantId); 15779 15780 // Update stock display with embedded data 15781 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15782 if (productId > 0) { 15783 updateStockDisplay(productId, currentVariantId); 15784 } 15785 15786 lastVariantId = currentVariantId; 15787 } 15788 }, 1000); 15789 15790 15791 } 15792 } 15793 15794 /** 15795 * Update stock display and enforce stock-based button blocking using embedded data 15796 */ 15797 function updateStockDisplay(productId, variantId = 0) { 15798 15799 15800 // Get stock info from embedded data (instant) 15801 const embeddedStockInfo = getStockFromEmbeddedData(variantId); 15802 15803 let stockInfo; 15804 if (embeddedStockInfo) { 15805 // Convert embedded data to standard format 15806 stockInfo = { 15807 can_add_to_cart: embeddedStockInfo.hasStock, 15808 is_in_stock: embeddedStockInfo.hasStock, 15809 available_stock: embeddedStockInfo.stock, 15810 message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, 15811 product_name: window.PD_NAME || 'Produk', 15812 sku: embeddedStockInfo.sku 15813 }; 15814 15815 15816 } else { 15817 // Fallback to API call if embedded data not available 15818 15819 checkStock(productId, variantId, 1) 15820 .then(apiStockInfo => { 15821 updateStockDisplayWithInfo(apiStockInfo, productId, variantId); 15822 }) 15823 .catch(error => { 15824 console.error('Failed to update stock display via API:', error); 15825 }); 15826 return; 15827 } 15828 15829 updateStockDisplayWithInfo(stockInfo, productId, variantId); 15830 } 15831 15832 /** 15833 * Update stock display with provided stock info 15834 */ 15835 function updateStockDisplayWithInfo(stockInfo, productId, variantId) { 15836 // Store current stock status globally for blocking checks 15837 window._currentStockInfo = stockInfo; 15838 15839 // Update stock indicators 15840 const stockIndicators = document.querySelectorAll('.stock-indicator'); 15841 stockIndicators.forEach(indicator => { 15842 if (stockInfo.is_in_stock) { 15843 indicator.classList.add('in-stock'); 15844 indicator.classList.remove('out-of-stock'); 15845 } else { 15846 indicator.classList.add('out-of-stock'); 15847 indicator.classList.remove('in-stock'); 15848 } 15849 }); 15850 15851 // Update stock count 15852 const stockCounts = document.querySelectorAll('.stock-count');
15853 stockCounts.forEach(count => { 15854 count.textContent = stockInfo.available_stock || 0; 15855 }); 15856 15857 // CRITICAL: For digital products, DO NOT modify button states here 15858 // PHP already handles button disable/enable based on LICENSE_POOL count 15859 // Only physical products need JavaScript stock validation 15860 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15861 15862 if (isDigitalProduct) { 15863 // For digital products: ONLY update global stock info, don't touch buttons 15864 // Let PHP-rendered disabled state remain authoritative 15865 console.log('[Stock Validator] Digital product detected - skipping button state modification'); 15866 return; // Exit early, preserve PHP-set button states 15867 } 15868 15869 // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 15870 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'); 15871 allButtons.forEach(button => { 15872 if (stockInfo.can_add_to_cart) { 15873 // STOCK AVAILABLE - ENABLE BUTTONS 15874 button.classList.remove(config.disabledClass); 15875 button.disabled = false; 15876 button.style.opacity = ''; 15877 button.style.cursor = ''; 15878 15879 // CRITICAL: Remove stock blocking attributes when stock becomes available 15880 button.removeAttribute('data-stock-blocked'); 15881 button.removeAttribute('data-stock-reason'); 15882 15883 15884 } else { 15885 // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING 15886 button.classList.add(config.disabledClass); 15887 button.disabled = true; 15888 button.style.opacity = '0.5'; 15889 button.style.cursor = 'not-allowed'; 15890 15891 // Mark as stock-blocked to prevent re-enabling 15892 button.setAttribute('data-stock-blocked', 'true'); 15893 button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); 15894 15895 15896 } 15897 }); 15898 15899 // Start aggressive button monitoring for out-of-stock variants 15900 if (!stockInfo.can_add_to_cart) { 15901 enforceStockDisabling(productId, variantId); 15902 } 15903 15904 15905 } 15906 15907 /** 15908 * Aggressively enforce button disabling for out-of-stock items 15909 * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) 15910 */ 15911 function enforceStockDisabling(productId, variantId) { 15912 // CRITICAL: Skip enforcement for digital products - PHP handles button states 15913 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15914 if (isDigitalProduct) { 15915 console.log('[Stock Validator] Digital product - skipping button enforcement'); 15916 return; // Exit early, let PHP-rendered disabled state remain 15917 } 15918 15919 const monitorInterval = setInterval(() => { 15920 // Check if variant changed 15921 const currentVariantSelector = document.getElementById('pd-variant-id'); 15922 const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; 15923 15924 if (currentVariantId !== variantId) { 15925 // Variant changed, stop monitoring this variant 15926 clearInterval(monitorInterval); 15927 15928 return; 15929 } 15930 15931 // Check current stock status using embedded data (real-time check) 15932 const currentStockInfo = getStockFromEmbeddedData(currentVariantId); 15933 15934 if (currentStockInfo && !currentStockInfo.hasStock) { 15935 // Still out of stock - continue monitoring for tampering 15936 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'); 15937 buttons.forEach(button => { 15938 // Check if button was manually enabled (inspect attack) 15939 if (!button.disabled || button.style.opacity !== '0.5') { 15940 15941 // Force re-disable 15942 button.disabled = true; 15943 button.style.opacity = '0.5'; 15944 button.style.cursor = 'not-allowed'; 15945 button.setAttribute('data-stock-blocked', 'true'); 15946 button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); 15947 15948 // Show warning toast 15949 if (window.showToast) { 15950 window.showToast({ 15951 type: 'warning',
15952 title: 'Aksi Tidak Diizinkan', 15953 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 15954 duration: 3000 15955 }); 15956 } 15957 } 15958 }); 15959 } else { 15960 // Stock available OR embedded data shows stock - stop monitoring 15961 clearInterval(monitorInterval); 15962 15963 // For PHYSICAL products only, enable buttons 15964 // For DIGITAL products, skip (PHP is authoritative) 15965 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15966 15967 if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 15968 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'); 15969 buttons.forEach(button => { 15970 button.disabled = false; 15971 button.style.opacity = ''; 15972 button.style.cursor = ''; 15973 button.removeAttribute('data-stock-blocked'); 15974 button.removeAttribute('data-stock-reason'); 15975 button.classList.remove(config.disabledClass); 15976 }); 15977 15978 } 15979 } 15980 }, 500); // Check every 500ms for tampering attempts 15981 15982 // Auto-stop monitoring after 30 seconds to prevent memory leaks 15983 setTimeout(() => { 15984 clearInterval(monitorInterval); 15985 15986 }, 30000); 15987 } 15988 15989 /** 15990 * Override addToCartSubmit function to add stock validation 15991 * This function now works with pd-addtocart.js integration 15992 */ 15993 function overrideAddToCartSubmitFunction() { 15994 // Check if pd-addtocart.js is already loaded 15995 const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 15996 window.addToCartSubmit.toString().includes('showToast'); 15997 15998 if (pdAddToCartExists) { 15999 16000 16001 // Store the pd-addtocart.js function 16002 const pdAddToCartSubmit = window.addToCartSubmit; 16003 window._pdAddToCartSubmit = pdAddToCartSubmit; 16004 16005 // Override with stock validation that calls pd-addtocart.js 16006 window.addToCartSubmit = async function(e, productId) { 16007 16008 // Don't prevent default here - let pd-addtocart handle it if stock is valid 16009 16010 try { 16011 // Extract product data from form (same way as pd-addtocart.js) 16012 const form = e.target; 16013 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16014 16015 // CRITICAL: Always get current variant from selector (real-time) 16016 let variantId = 0; 16017 const variantSelector = document.getElementById('pd-variant-id'); 16018 if (variantSelector) { 16019 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16020 } 16021 16022 if (!productId || productId <= 0) { 16023 return pdAddToCartSubmit(e, productId); 16024 } 16025 16026 16027 // Validate stock before proceeding 16028 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16029 16030 if (isValid) { 16031 // Stock OK, call pd-addtocart.js function (it will handle toast) 16032 return pdAddToCartSubmit(e, productId); 16033 } else { 16034 // Prevent the form submission 16035 e.preventDefault(); 16036 // Stock validation failed, modal already shown by validateBeforeCart 16037 return false; 16038 } 16039 16040 } catch (error) { 16041 e.preventDefault(); 16042 // On error, show modal and don't proceed 16043 showStockModal({
16044 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16045 is_active: false 16046 }, 'cart'); 16047 return false; 16048 } 16049 }; 16050 } else { 16051 // Original logic for when pd-addtocart.js is not present 16052 const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; 16053 16054 if (originalAddToCartSubmit) { 16055 window._originalAddToCartSubmit = originalAddToCartSubmit; 16056 } 16057 16058 // Override with stock validation 16059 window.addToCartSubmit = async function(e, productId) { 16060 16061 e.preventDefault(); 16062 16063 try { 16064 const form = e.target; 16065 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16066 16067 // CRITICAL: Always get current variant from selector (real-time) 16068 let variantId = 0; 16069 const variantSelector = document.getElementById('pd-variant-id'); 16070 if (variantSelector) { 16071 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16072 } 16073 16074 if (!productId || productId <= 0) { 16075 if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); 16076 return false; 16077 } 16078 16079 16080 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16081 16082 if (isValid) { 16083 if (originalAddToCartSubmit) { 16084 return originalAddToCartSubmit(e, productId); 16085 } else { 16086 if (typeof window.addToCart === 'function') { 16087 await window.addToCart(productId, qty, variantId); 16088 } 16089 } 16090 } else { 16091 } 16092 16093 } catch (error) { 16094 showStockModal({ 16095 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16096 is_active: false 16097 }, 'cart'); 16098 } 16099 16100 return false; 16101 }; 16102 } 16103 16104 16105 } 16106 16107 /** 16108 * Override buyNow function to add stock validation 16109 */ 16110 function overrideBuyNowFunction() { 16111 // Store original buyNow function if it exists 16112 const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; 16113 16114 // Override with stock validation 16115 window.buyNow = async function(e) { 16116 16117 try { 16118 // Get product data (same way as pd-buynow.js) 16119 const form = document.getElementById('pd-buy-form'); 16120 if (!form) { 16121 if (originalBuyNow) return originalBuyNow(e); 16122 return; 16123 } 16124 16125 const productId = parseInt(form.dataset.pid || '0', 10); 16126 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16127 16128 // CRITICAL: Always get current variant from selector (real-time) 16129 let variantId = 0; 16130 const variantSelector = document.getElementById('pd-variant-id'); 16131 if (variantSelector) { 16132 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16133 } 16134 16135 if (!productId || productId <= 0) { 16136 if (originalBuyNow) return originalBuyNow(e); 16137 return; 16138 } 16139 16140 16141 // Validate stock before proceeding 16142 const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); 16143 16144 if (isValid) { 16145 // Stock OK, call original buyNow function 16146 if (originalBuyNow) { 16147 return originalBuyNow(e); 16148 } else { 16149 } 16150 } else { 16151 // Stock validation failed, modal already shown by validateBeforeCart 16152 } 16153 16154 } catch (error) { 16155 // On error, show modal and don't proceed 16156 showStockModal({
16157 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16158 is_active: false 16159 }, 'buy'); 16160 } 16161 }; 16162 16163 16164 } 16165 16166 /** 16167 * Initialize stock validation 16168 */ 16169 function init() { 16170 // Wait for DOM ready 16171 if (document.readyState === 'loading') { 16172 addEventListener(document, 'DOMContentLoaded', function() { 16173 initializeButtonAttributes(); 16174 setupVariantChangeIntegration(); // Setup variant change integration 16175 interceptCartButtons(); 16176 setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load 16177 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16178 setTimeout(() => { 16179 overrideAddToCartSubmitFunction(); 16180 overrideBuyNowFunction(); 16181 }, 100); 16182 16183 setTimeout(() => { 16184 // Re-check and re-override if pd-addtocart.js loaded later 16185 overrideAddToCartSubmitFunction(); 16186 }, 500); 16187 16188 setTimeout(() => { 16189 // Final check for very late loading scripts 16190 overrideAddToCartSubmitFunction(); 16191 }, 1000); 16192 16193 16194 }); 16195 } else { 16196 initializeButtonAttributes(); 16197 setupVariantChangeIntegration(); // Setup variant change integration 16198 interceptCartButtons(); 16199 setupGlobalClickInterceptor(); // Setup global click security interceptor 16200 16201 // Override functions after other scripts load 16202 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16203 setTimeout(() => { 16204 overrideAddToCartSubmitFunction(); 16205 overrideBuyNowFunction(); 16206 }, 100); 16207 16208 setTimeout(() => { 16209 // Re-check and re-override if pd-addtocart.js loaded later 16210 overrideAddToCartSubmitFunction(); 16211 }, 500); 16212 16213 setTimeout(() => { 16214 // Final check for very late loading scripts 16215 overrideAddToCartSubmitFunction(); 16216 }, 1000); 16217 16218 16219 } 16220 } 16221 16222 // Public API 16223 return { 16224 init: init, 16225 checkStock: checkStock, 16226 validateBeforeCart: validateBeforeCart, 16227 updateStockDisplay: updateStockDisplay, 16228 showStockModal: showStockModal, 16229 config: config 16230 }; 16231})(); 16232 16233// Auto-initialize 16234if (typeof window !== 'undefined') { 16235 StockValidator.init(); 16236} 16237 16238</script>
vendor: 2 bytes, line 16238
16238
16239<script data-inlined="pd-addtocart.js"> 16240(function(){ 16241 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16242 function getVariantSummaryText(){ 16243 const vs = document.getElementById('pd-variant-summary'); 16244 if (!vs) return ''; 16245 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16246 return items.length ? items.join(' / ') : ''; 16247 } 16248 window.addToCartSubmit = async function(e, productId){ 16249 e.preventDefault(); 16250 try { 16251 const form = e.target; 16252 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16253 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16254 await (window.addToCart ? window.addToCart(productId, qty, variantId) : Promise.reject(new Error('addToCart tidak tersedia'))); 16255 16256 if (window.PlazaAnalytics) { 16257 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16258 } 16259 16260 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16261 const sku = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim(): (window.PD_SKU||''); })(); 16262 const variantText = getVariantSummaryText(); 16263 16264 if (window.showToast) { 16265 window.showToast({ 16266 type: 'success', 16267 title: 'Berhasil ditambahkan ke Keranjang', 16268 message: `<strong>${esc(name)}</strong>${variantText?` · <em>${esc(variantText)}</em>`:''}<br>SKU: ${esc(sku)} · Qty: ${qty}`, 16269 duration: 5000, 16270 actions: [ 16271 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 16272 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 16273 ] 16274 }); 16275 } 16276 } catch (err) { 16277 if (window.showToast) { 16278 window.showToast({ type:'warn', title:'Gagal menambahkan', message: String(err?.message||'Terjadi kesalahan'), duration: 4500 }); 16279 } else { 16280 console.error(err); 16281 } 16282 } 16283 return false; 16284 }; 16285})(); 16286</script>
vendor: 2 bytes, line 16286
16286
16287<script data-inlined="pd-buynow.js"> 16288(function(){ 16289 16290 // CSRF Token Helper 16291 function getCsrfToken() { 16292 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 16293 } 16294 16295 // Utility functions 16296 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16297 16298 function getVariantSummaryText(){ 16299 const vs = document.getElementById('pd-variant-summary'); 16300 if (!vs) return ''; 16301 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16302 return items.length ? items.join(' / ') : ''; 16303 } 16304 16305 function getCurrentProductData(){ 16306 const form = document.getElementById('pd-buy-form'); 16307 if (!form) return null; 16308 16309 const productId = parseInt(form.dataset.pid || '0', 10); 16310 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16311 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16312 16313 if (!productId || productId <= 0) { 16314 throw new Error('Product ID tidak valid'); 16315 } 16316 16317 if (qty <= 0) { 16318 throw new Error('Quantity harus lebih dari 0'); 16319 } 16320 16321 return { productId, qty, variantId }; 16322 } 16323 16324 function createBuyNowPayload(productId, qty, variantId) { 16325 // Get product metadata 16326 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16327 16328 // Get correct price, SKU, and weight from selected variant data instead of DOM elements 16329 let price = 0; 16330 let sku = ''; 16331 let weight = 0; 16332 let dimensions = null; 16333 16334 if (variantId > 0 && typeof window.variants !== 'undefined' && Array.isArray(window.variants)) { 16335 // Find the selected variant in the variants array 16336 const selectedVariant = window.variants.find(v => v.id === variantId); 16337 if (selectedVariant) { 16338 price = selectedVariant.final || 0; 16339 sku = selectedVariant.sku || ''; 16340 weight = selectedVariant.weight || 0; 16341 16342 // Include dimensions if available 16343 if (selectedVariant.length_cm || selectedVariant.width_cm || selectedVariant.height_cm) { 16344 dimensions = { 16345 length_cm: selectedVariant.length_cm || 0, 16346 width_cm: selectedVariant.width_cm || 0, 16347 height_cm: selectedVariant.height_cm || 0 16348 }; 16349 } 16350 16351 // Debug info to verify fix is working 16352 if (window.console && window.console.debug) { 16353 console.debug('BuyNow: Using variant data', { 16354 variantId: variantId, 16355 price: price, 16356 sku: sku, 16357 weight: weight, 16358 dimensions: dimensions, 16359 variantData: selectedVariant 16360 }); 16361 } 16362 } 16363 } 16364 16365 // Fallback to DOM elements if variant data not found 16366 if (price === 0) { 16367 const priceTxt = document.getElementById('pd-price-current')?.textContent || ''; 16368 price = parseInt(String(priceTxt).replace(/[^\d]/g,''), 10) || 0; 16369 16370 if (window.console && window.console.debug) { 16371 console.debug('BuyNow: Using DOM price fallback', { 16372 priceTxt: priceTxt, 16373 price: price, 16374 variantId: variantId, 16375 reason: variantId > 0 ? 'variant_data_not_found' : 'no_variant_selected' 16376 }); 16377 } 16378 } 16379 16380 if (!sku) { 16381 const skuEl = document.getElementById('pd-sku'); 16382 sku = skuEl ? skuEl.textContent.trim() : (window.PD_SKU || ''); 16383 } 16384 16385 const imgEl = document.getElementById('pd-main-img'); 16386 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 16387 16388 const variantText = getVariantSummaryText(); 16389 16390 // Get pre-order data if product is pre-order 16391 const preorderData = window.PD_PREORDER_DATA || null; 16392 16393 return { 16394 product_id: productId, 16395 qty: qty, 16396 variant_id: variantId, 16397 meta: { 16398 name, 16399 sku, 16400 price, 16401 image_url, 16402 variant_text: variantText, 16403 weight: weight, 16404 dimensions: dimensions 16405 }, 16406 is_preorder: preorderData ? 'Y' : 'N', 16407 preorder_estimated_arrival: preorderData ? preorderData.estimated_arrival : null, 16408 preorder_note: preorderData ? preorderData.note : '' 16409 }; 16410 } 16411 16412 /** 16413 * Main Buy Now function
16414 * Performs validation and redirects to checkout with buy_now mode 16415 */ 16416 window.buyNow = async function(e) { 16417 if (e) e.preventDefault(); 16418 16419 try { 16420 // Check user login status 16421 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16422 document.body.classList.contains('logged-in') || 16423 window.userLoggedIn === true || 16424 document.body.dataset.userLoggedIn === 'true'; 16425 16426 if (!isLoggedIn) { 16427 // Redirect to login with return URL to current page 16428 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16429 window.location.href = '/login?return=' + currentUrl; 16430 return; 16431 } 16432 16433 // Get current product data 16434 const { productId, qty, variantId } = getCurrentProductData(); 16435 16436 // Create payload for buy now 16437 const payload = createBuyNowPayload(productId, qty, variantId); 16438 16439 // Store buy now data in sessionStorage for checkout page 16440 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16441 16442 // Track add_to_cart (buy now counts as purchase intent) 16443 if (window.PlazaAnalytics) { 16444 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16445 } 16446 16447 // Redirect to checkout with buy_now mode 16448 window.location.href = '/checkout?mode=buy_now'; 16449 16450 } catch (err) { 16451 console.error('Buy Now error:', err); 16452 16453 if (window.showToast) { 16454 window.showToast({ 16455 type: 'warn', 16456 title: 'Gagal memproses', 16457 message: String(err?.message || 'Terjadi kesalahan saat memproses pembelian'), 16458 duration: 4500 16459 }); 16460 } else { 16461 alert(err.message || 'Terjadi kesalahan saat memproses pembelian'); 16462 } 16463 } 16464 }; 16465 16466 /** 16467 * Alternative Buy Now with confirmation dialog 16468 */ 16469 window.buyNowWithConfirm = async function(e) { 16470 if (e) e.preventDefault(); 16471 16472 try { 16473 const { productId, qty, variantId } = getCurrentProductData(); 16474 16475 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16476 const variantText = getVariantSummaryText(); 16477 const priceTxt = document.getElementById('pd-price-current')?.textContent || 'Rp0'; 16478 16479 const confirmMessage = `Lanjut ke checkout?\n\nProduk: ${name}${variantText ? `\nVariant: ${variantText}` : ''}\nQty: ${qty}\nHarga: ${priceTxt}`; 16480 16481 if (!confirm(confirmMessage)) { 16482 return; 16483 } 16484 16485 // Proceed with buy now 16486 await window.buyNow(e); 16487 16488 } catch (err) { 16489 console.error('Buy Now with confirm error:', err); 16490 alert(err.message || 'Terjadi kesalahan'); 16491 } 16492 }; 16493 16494 /** 16495 * Attach event listener for Buy with Voucher button 16496 */ 16497 function setupBuyButtons() { 16498 // Setup desktop buy button (regular buy now without voucher) 16499 const buyBtn = document.getElementById('btn-buy'); 16500 if (buyBtn) { 16501 buyBtn.addEventListener('click', function(e) { 16502 e.preventDefault(); 16503 if (buyBtn.disabled) return; 16504 if (window.buyNow) { 16505 window.buyNow(e); 16506 } 16507 }); 16508 } 16509 16510 // Setup desktop buy with voucher button 16511 const voucherBtn = document.getElementById('btn-buy-voucher'); 16512 if (voucherBtn) { 16513 voucherBtn.addEventListener('click', function(e) { 16514 e.preventDefault(); 16515 16516 // Validate stock availability before proceeding 16517 if (this.disabled || this.getAttribute('data-stock-blocked') === 'true') { 16518 const stockReason = this.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16519 16520 if (window.showToast) { 16521 window.showToast({ 16522 type: 'warn', 16523 title: 'Stok Tidak Tersedia', 16524 message: stockReason, 16525 duration: 3500 16526 }); 16527 } else { 16528 alert(stockReason); 16529 } 16530 return; 16531 } 16532 16533 const voucherCode = this.dataset.voucherCode; 16534 if (voucherCode) { 16535 window.buyNowWithVoucher(e, voucherCode); 16536 } 16537 }); 16538 } 16539 16540 } 16541 16542 // CRITICAL: Attach mobile button handler at DOCUMENT level with CAPTURE phase 16543 // This is the ONLY way to guarantee it fires regardless of other scripts 16544 document.addEventListener('click', function(e) { 16545 // Check if click is on mobile buy button or its children 16546 const target = e.target.closest('#mf-buy'); 16547 if (!target) return; // Not our button 16548 16549 e.preventDefault(); 16550 e.stopPropagation(); 16551 e.stopImmediatePropagation(); 16552 16553 if (target.disabled) return; 16554
16555 // Get voucher code from parent FAB's data attribute 16556 const mobileFab = document.getElementById('pd-mobile-fab'); 16557 const voucherCode = mobileFab ? (mobileFab.getAttribute('data-voucher-code') || '') : ''; 16558 16559 if (voucherCode && voucherCode.trim() !== '' && window.buyNowWithVoucher) { 16560 window.buyNowWithVoucher(e, voucherCode); 16561 } else if (window.buyNow) { 16562 window.buyNow(e); 16563 } 16564 }, true); // CAPTURE PHASE - fires BEFORE any bubble phase handlers 16565 16566 // Execute immediately if DOM already loaded, otherwise wait 16567 if (document.readyState === 'loading') { 16568 document.addEventListener('DOMContentLoaded', setupBuyButtons); 16569 } else { 16570 setupBuyButtons(); 16571 } 16572 16573 /** 16574 * Buy Now with Auto-Applied Voucher 16575 * Applies voucher to session before redirecting to checkout (like cart does) 16576 * 16577 * @param {Event} e - Click event 16578 * @param {string} voucherCode - Voucher code to apply 16579 */ 16580 window.buyNowWithVoucher = async function(e, voucherCode) { 16581 if (e) e.preventDefault(); 16582 16583 console.log('[buyNowWithVoucher] Called with voucher code:', voucherCode); 16584 16585 try { 16586 // CRITICAL: Check stock availability first 16587 const voucherBtn = document.getElementById('btn-buy-voucher'); 16588 if (voucherBtn) { 16589 if (voucherBtn.disabled || voucherBtn.getAttribute('data-stock-blocked') === 'true') { 16590 const stockReason = voucherBtn.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16591 16592 if (window.showToast) { 16593 window.showToast({ 16594 type: 'warn', 16595 title: 'Stok Tidak Tersedia', 16596 message: stockReason, 16597 duration: 3500 16598 }); 16599 } else { 16600 alert(stockReason); 16601 } 16602 return; 16603 } 16604 } 16605 16606 // Check login status first 16607 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16608 document.body.classList.contains('logged-in') || 16609 window.userLoggedIn === true || 16610 document.body.dataset.userLoggedIn === 'true'; 16611 16612 console.log('[buyNowWithVoucher] User logged in:', isLoggedIn); 16613 16614 if (!isLoggedIn) { 16615 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16616 window.location.href = '/login?return=' + currentUrl; 16617 return; 16618 } 16619 16620 // Get current product data 16621 const { productId, qty, variantId } = getCurrentProductData(); 16622 const payload = createBuyNowPayload(productId, qty, variantId); 16623 16624 console.log('[buyNowWithVoucher] Product data:', { productId, qty, variantId }); 16625 16626 // Get category path for voucher validation 16627 const categoryPath = window.PD_CATEGORY_PATH || ''; 16628 16629 // Step 1: Clear any existing voucher first (important for clean state) 16630 console.log('[buyNowWithVoucher] Step 1: Clearing existing voucher...'); 16631 try { 16632 await fetch('/api/voucher/remove.php', { 16633 method: 'POST', 16634 headers: { 16635 'X-CSRF-Token': getCsrfToken() 16636 }, 16637 body: JSON.stringify({ 16638 csrf_token: getCsrfToken() 16639 }), 16640 credentials: 'same-origin' 16641 }); 16642 } catch (e) { 16643 // Ignore error if no voucher exists 16644 console.debug('No existing voucher to remove or removal failed'); 16645 } 16646 16647 // Step 2: Apply voucher to session (like cart does) 16648 console.log('[buyNowWithVoucher] Step 2: Applying voucher to session...'); 16649 const applyRes = await fetch('/api/voucher/apply.php', { 16650 method: 'POST', 16651 headers: { 16652 'Content-Type': 'application/json', 16653 'X-CSRF-Token': getCsrfToken() 16654 }, 16655 credentials: 'same-origin', 16656 body: JSON.stringify({ 16657 csrf_token: getCsrfToken(), 16658 code: voucherCode, 16659 items: [{ 16660 pid: productId, 16661 price: payload.meta.price, 16662 qty: qty, 16663 category_path: categoryPath 16664 }], 16665 shipping_cost: null // Will be calculated in checkout 16666 }) 16667 }); 16668 16669 const applyData = await applyRes.json(); 16670 16671 console.log('[buyNowWithVoucher] Voucher apply response:', applyData); 16672 16673 if (!applyData.success) { 16674 // Voucher invalid - show error and fallback to regular buy now 16675 console.warn('[buyNowWithVoucher] Voucher application failed:', applyData.message); 16676 if (window.showToast) { 16677 window.showToast({ 16678 type: 'error', 16679 title: 'Voucher Tidak Valid', 16680 message: applyData.message || 'Voucher tidak dapat diterapkan untuk produk ini', 16681 duration: 4000 16682 }); 16683 } else { 16684 alert(applyData.message || 'Voucher tidak dapat diterapkan'); 16685 } 16686 16687 // Fallback to regular buy now without voucher 16688 console.log('Voucher failed, proceeding with regular buy now'); 16689 return window.buyNow(e); 16690 } 16691 16692 // Step 3: Store buy now data in sessionStorage 16693 console.log('[buyNowWithVoucher] Step 3: Storing buy now data in sessionStorage'); 16694 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16695 16696 // Step 4: Redirect to checkout 16697 // Voucher sudah di session, checkout akan auto-load via loadAppliedVoucher() 16698 console.log('[buyNowWithVoucher] Step 4: Redirecting to checkout with voucher applied'); 16699 window.location.href = '/checkout?mode=buy_now'; 16700 16701 } catch (err) { 16702 console.error('Buy Now with Voucher error:', err); 16703 16704 if (window.showToast) { 16705 window.showToast({ 16706 type: 'error', 16707 title: 'Terjadi Kesalahan', 16708 message: 'Gagal memproses pembelian dengan voucher. Silakan coba lagi.', 16709 duration: 3000 16710 }); 16711 } else { 16712 alert('Gagal memproses pembelian. Silakan coba lagi.'); 16713 } 16714 } 16715 }; 16716})(); 16717</script>
vendor: 2 bytes, line 16717
16717
16718<script data-inlined="pd-voucher-recalc.js"> 16719/** 16720 * Product Detail - Voucher Discount Recalculation 16721 * 16722 * Handles dynamic voucher discount recalculation when variant price changes 16723 * Supports both PERCENTAGE and FIXED_AMOUNT voucher types 16724 */ 16725 16726(function() { 16727 'use strict'; 16728 16729 /** 16730 * Calculate voucher discount based on type and price 16731 * 16732 * @param {string} type - Voucher type: 'PERCENTAGE' or 'FIXED_AMOUNT' 16733 * @param {number} value - Voucher value (percentage or fixed amount) 16734 * @param {number} price - Current variant price 16735 * @param {number} maxDiscount - Maximum discount for percentage vouchers (0 = no limit) 16736 * @return {number} Calculated discount amount 16737 */ 16738 function calculateVoucherDiscount(type, value, price, maxDiscount) { 16739 if (type === 'PERCENTAGE') { 16740 // Calculate percentage discount 16741 let discount = (price * value) / 100; 16742 16743 // Apply max discount cap if set 16744 if (maxDiscount > 0 && discount > maxDiscount) { 16745 discount = maxDiscount; 16746 } 16747 16748 return Math.round(discount); 16749 } else { 16750 // FIXED_AMOUNT - discount doesn't change with price 16751 return Math.round(value); 16752 } 16753 } 16754 16755 /** 16756 * Format number as Rupiah without 'Rp' prefix 16757 * 16758 * @param {number} num - Number to format 16759 * @return {string} Formatted number (e.g., "20.000") 16760 */ 16761 function formatNumber(num) { 16762 return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 16763 } 16764 16765 /** 16766 * Update voucher button discount label 16767 * 16768 * @param {number} newPrice - New variant price 16769 */ 16770 function updateVoucherDiscount(newPrice) { 16771 const voucherBtn = document.getElementById('btn-buy-voucher'); 16772 if (!voucherBtn) return; 16773 16774 // Get voucher data from button attributes 16775 const voucherType = voucherBtn.dataset.voucherType; 16776 const voucherValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 16777 const voucherMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 16778 16779 if (!voucherType || voucherValue === 0) return; 16780 16781 // Calculate new discount 16782 const newDiscount = calculateVoucherDiscount(voucherType, voucherValue, newPrice, voucherMax); 16783 16784 // Update the discount label 16785 const saveLabel = voucherBtn.querySelector('.bv-save'); 16786 if (saveLabel) { 16787 saveLabel.textContent = '-Rp' + formatNumber(newDiscount); 16788 } 16789 16790 // Also update mobile FAB voucher display if exists 16791 updateMobileFabVoucher(newDiscount); 16792 } 16793 16794 /** 16795 * Update mobile FAB voucher value 16796 * 16797 * @param {number} discountAmount - New discount amount 16798 */ 16799 function updateMobileFabVoucher(discountAmount) { 16800 const fab = document.getElementById('pd-mobile-fab'); 16801 if (fab) { 16802 fab.dataset.voucherValue = discountAmount.toString(); 16803 } 16804 } 16805 16806 /** 16807 * Listen to variant change events and recalculate discount 16808 */ 16809 function initVoucherRecalculation() { 16810 window.addEventListener('pd-variant-changed', function(e) { 16811 const detail = e.detail || {}; 16812 const newPrice = detail.price || 0; 16813 16814 if (newPrice > 0) { 16815 updateVoucherDiscount(newPrice); 16816 } 16817 }); 16818 } 16819 16820 // Initialize when DOM is ready 16821 if (document.readyState === 'loading') { 16822 document.addEventListener('DOMContentLoaded', initVoucherRecalculation); 16823 } else { 16824 initVoucherRecalculation(); 16825 } 16826 16827})(); 16828 16829</script>
vendor: 2 bytes, line 16829
16829
16830<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>
16830 16831
16832<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16833(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'}})}} 16834 16835function _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()}})(); 16836 16837(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()}})})(); 16838</script>
16838<footer class="site-footer footer-compact variant-stripes" role="contentinfo"> 16839 <div class="ft-top ft-collapsible" id="ftTop"> 16840 <div class="container ft-grid"> 16841 16842 <div class="ft-col ft-help"> 16843 <h4 class="ft-title">Butuh Bantuan?</h4> 16844 <p class="ft-desc">Hubungi kami melalui Official WhatsApp interaktif sekarang.</p> 16845 <a class="ft-wa-btn" href="https://calcourtsfitness.nichesite.org/" target="_blank" rel="nofollow noopener"> 16846 <span class="ico" aria-hidden="true"> 16847 <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> 16848 </span> 16849 Kirim Pesan 16850 </a> 16851 16852 <p class="ft-desc sm-gap">Ikuti kami:</p> 16853 <div class="ft-social"> 16854 <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> 16855 <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
16855V7.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> 16856 <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> 16857 </div> 16858 </div> 16859 16860 <!-- Collapsible Content Wrapper (Non-Homepage Only) --> 16861 <div class="ft-collapsible-content" id="ftCollapsible"> 16862 16863 <div class="ft-col"> 16864 <h4 class="ft-title">Tentang PIONTOGEL</h4> 16865 <ul class="ft-links"> 16866 <li><a href="https://calcourtsfitness.com/">Tentang PIONTOGEL</a></li> 16867 <li><a href="https://calcourtsfitness.com/">Kontak PIONTOGEL</a></li> 16868 <li><a href="https://calcourtsfitness.com/">Lokasi Toko</a></li> 16869 <li><a href="https://calcourtsfitness.com/">FAQ</a></li> 16870 </ul> 16871 <div class="ft-address"> 16872 <p class="company">PT.ANGIN MALAM</p> 16873 <p>JL. Harapan Blok Melati No. 27, Kel. Pademangan Barat,</p> 16874 <p>Kec. Jakarta, Kota Jakarta, DKI JAKARTA, 11752</p> 16875 </div> 16876 </div> 16877 16878 <div class="ft-col"> 16879 <h4 class="ft-title">Informasi</h4> 16880 <ul class="ft-links"> 16881 <li><a href="/syarat-dan-ketentuan">Syarat & Ketentuan</a></li> 16882 <li><a href="/kebijakan-privasi">Kebijakan Privasi</a></li> 16883 <li><a href="/kebijakan-cookie">Kebijakan Cookie</a></li> 16884 <li><a href="/pengiriman-dan-pengembalian">Pengiriman & Pengembalian</a></li> 16885 <li><a href="/pembayaran-dan-cicilan">Pembayaran & Cicilan</a></li> 16886 <li><a href="/garansi-dan-layanan">Garansi & Layanan</a></li> 16887 </ul> 16888 16889 <div class="ft-payments payments-small" aria-label="Metode Pembayaran"> 16890 <div class="pay-row"> 16891 <img src="https://xn--wcsp1x05m74d.live/images/2026/07/18/mandiri-PIONTOGEL.png" alt="Mandiri" loading="lazy"> 16892 <img src="https://xn--wcsp1x05m74d.live/images/2026/07/18/bni-PIONTOGEL.gif" alt="BNI" loading="lazy"> 16893 <img src="https://xn--wcsp1x05m74d.live/images/2026/07/18/bri-PIONTOGEL.png" alt="BRI" loading="lazy"> 16894 <img src="https://xn--wcsp1x05m74d.live/images/2026/07/18/qris-PIONTOGEL.gif" alt="QRIS" loading="lazy"> 16895 <img src="https://xn--wcsp1x05m74d.live/images/2026/07/18/bca-PIONTOGEL.gif" alt="BCA" loading="lazy"> 16896 </div> 16897 </div> 16898 </div> 16899 16900 </div> 16901 <!-- End Collapsible Content --> 16902 16903 <!-- Expand Button (Mobile Only) --> 16904 <button class="ft-expand-btn" id="ftExpandBtn" aria-label="Lihat semua informasi footer"> 16905 <span class="expand-text">Selengkapnya</span> 16906 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 16907 <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> 16908 </svg> 16909 </button> 16910 16911 </div> 16912 </div> 16913 <div class="ft-bottom"> 16914 <div class="container ft-bottom-inner"> 16915 <div class="ft-copy"> 16916 <span>ANGIN-MALAM © <span id="yearNow">2026</span></span> 16917 <span>PT ANGIN-MALAM. All rights reserved.</span> 16918 </div> 16919 </div> 16920 </div> 16921 16922 <style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16923 /* Chat Widget Styles - Lightweight & Minimal */ 16924 .chat-widget { 16925 position: fixed; 16926 bottom: 20px; 16927 right: 20px; 16928 z-index: 9999; 16929 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 16930 } 16931 16932 /* Hide floating button on mobile - use bottom nav instead */ 16933 @media (max-width: 640px) { 16934 .chat-toggle-btn { 16935 display: none !important; 16936 } 16937 16938 /* Ensure popup is still visible on mobile even when button is hidden */ 16939 .chat-widget { 16940 display: block !important; 16941 } 16942 16943 .chat-popup { 16944 display: none; /* Hidden by default, shown via JS */ 16945 } 16946 16947 .chat-popup[style*="display: block"], 16948 .chat-popup[style*="display:block"] { 16949 display: block !important; 16950 } 16951 } 16952 16953 .chat-toggle-btn { 16954 width: 60px; 16955 height: 60px; 16956 border-radius: 50%; 16957 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 16958 border: none; 16959 color: white; 16960 cursor: pointer; 16961 box-shadow: 0 4px 12px rgba(242, 140, 26, 0.3); 16962 position: relative; 16963 transition: transform 0.2s, box-shadow 0.2s; 16964 } 16965 16966 .chat-toggle-btn:hover { 16967 transform: scale(1.05); 16968 box-shadow: 0 6px 20px rgba(242, 140, 26, 0.4); 16969 } 16970 16971 #chat-toggle-btn .chat-icon { 16972 position: absolute; 16973 top: 50%; 16974 left: 50%; 16975 transform: translate(-50%, -50%); 16976 width: 28px !important; 16977 height: 28px !important; 16978 } 16979 16980 .chat-unread-badge { 16981 position: absolute; 16982 top: -5px; 16983 right: -5px; 16984 background: #ef4444; 16985 color: white; 16986 border-radius: 12px; 16987 padding: 2px 6px; 16988 font-size: 11px; 16989 font-weight: 600;
16990 min-width: 20px; 16991 text-align: center; 16992 } 16993 16994 .chat-popup { 16995 position: absolute; 16996 bottom: 80px; 16997 right: 0; 16998 width: 380px; 16999 max-width: calc(100vw - 40px); 17000 height: 500px; 17001 max-height: calc(100vh - 120px); 17002 background: white; 17003 border-radius: 12px; 17004 box-shadow: 0 10px 40px rgba(0,0,0,0.2); 17005 display: flex; 17006 flex-direction: column; 17007 overflow: hidden; 17008 } 17009 17010 .chat-popup-header { 17011 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17012 color: white; 17013 padding: 16px 20px; 17014 display: flex; 17015 justify-content: space-between; 17016 align-items: center; 17017 flex-shrink: 0; 17018 } 17019 17020 .chat-header-title { 17021 display: flex; 17022 flex-direction: column; 17023 gap: 2px; 17024 min-height: 0; 17025 } 17026 17027 .chat-popup-header h3 { 17028 margin: 0; 17029 font-size: 18px; 17030 font-weight: 600; 17031 line-height: 1.2; 17032 } 17033 17034 .chat-operating-hours { 17035 font-size: 11px; 17036 opacity: 0.9; 17037 font-weight: 400; 17038 display: flex; 17039 align-items: center; 17040 gap: 4px; 17041 line-height: 1.2; 17042 } 17043 17044 .chat-operating-hours svg { 17045 width: 12px; 17046 height: 12px; 17047 opacity: 0.8; 17048 flex-shrink: 0; 17049 } 17050 17051 .chat-header-actions { 17052 display: flex; 17053 align-items: center; 17054 gap: 8px; 17055 } 17056 17057 /* Notification Toggle Button */ 17058 .btn-notif-toggle { 17059 background: none; 17060 border: none; 17061 color: white; 17062 font-size: 18px; 17063 cursor: pointer; 17064 padding: 4px 8px; 17065 transition: all 0.2s; 17066 opacity: 0.8; 17067 } 17068 17069 .btn-notif-toggle:hover { 17070 opacity: 1; 17071 transform: scale(1.1); 17072 } 17073 17074 /* Icon states */ 17075 .btn-notif-toggle[data-status="not-subscribed"] svg { 17076 opacity: 0.7; 17077 } 17078 17079 .btn-notif-toggle[data-status="not-subscribed"]:hover svg { 17080 opacity: 1; 17081 } 17082 17083 .btn-notif-toggle[data-status="subscribed"] svg { 17084 opacity: 1; 17085 animation: bellRing 2s ease-in-out; 17086 } 17087 17088 .btn-notif-toggle[data-status="blocked"] svg { 17089 color: #ff6b6b; 17090 } 17091 17092 .btn-notif-toggle[data-status="not-supported"] { 17093 display: none !important; 17094 } 17095 17096 @keyframes bellRing { 17097 0%, 100% { transform: rotate(0deg); } 17098 10%, 30% { transform: rotate(-10deg); } 17099 20%, 40% { transform: rotate(10deg); } 17100 } 17101 17102 /* Notification Info Bar */ 17103 .chat-notif-info-bar { 17104 position: absolute; 17105 top: 64px; 17106 left: 0; 17107 right: 0; 17108 background: linear-gradient(to right, #e3f2fd, #f5f5f5); 17109 border-bottom: 1px solid #bbdefb; 17110 padding: 8px 12px; 17111 display: flex; 17112 align-items: center; 17113 gap: 8px; 17114 font-size: 12px; 17115 color: #616161; 17116 z-index: 10; 17117 animation: slideDown 0.3s ease; 17118 } 17119 17120 .chat-notif-info-bar svg { 17121 flex-shrink: 0; 17122 } 17123 17124 .chat-notif-info-bar .small { 17125 flex: 1; 17126 line-height: 1.4; 17127 } 17128 17129 .chat-notif-info-bar .btn-dismiss { 17130 background: none; 17131 border: none; 17132 color: #9e9e9e; 17133 cursor: pointer; 17134 padding: 0 4px; 17135 font-size: 16px; 17136 transition: color 0.2s; 17137 } 17138 17139 .chat-notif-info-bar .btn-dismiss:hover { 17140 color: #616161; 17141 } 17142 17143 @keyframes slideDown { 17144 from { 17145 max-height: 0; 17146 opacity: 0; 17147 } 17148 to { 17149 max-height: 50px; 17150 opacity: 1;
17151 } 17152 } 17153 17154 .chat-close-btn { 17155 background: none; 17156 border: none; 17157 color: white; 17158 font-size: 28px; 17159 line-height: 1; 17160 cursor: pointer; 17161 padding: 0; 17162 width: 30px; 17163 height: 30px; 17164 } 17165 17166 .chat-popup-body { 17167 flex: 1 1 auto; 17168 display: flex; 17169 flex-direction: column; 17170 min-height: 0; 17171 overflow: hidden; 17172 height: calc(100% - 64px); 17173 position: relative; 17174 transition: padding-top 0.3s ease; 17175 } 17176 17177 /* Add padding when info bar is visible */ 17178 .chat-popup-body.has-info-bar { 17179 padding-top: 40px; 17180 } 17181 17182 .chat-view { 17183 flex: 1 1 auto; 17184 display: none; 17185 flex-direction: column; 17186 min-height: 0; 17187 overflow: hidden; 17188 } 17189 17190 #chat-list-view { 17191 display: flex; 17192 } 17193 17194 #chat-messages-view { 17195 display: none; 17196 } 17197 17198 #chat-messages-view.active { 17199 display: flex; 17200 flex-direction: column; 17201 } 17202 17203 #chat-new-view { 17204 display: none; 17205 } 17206 17207 #chat-new-view.active { 17208 display: flex; 17209 } 17210 17211 .chat-conversations-list { 17212 flex: 1; 17213 overflow-y: auto; 17214 padding: 0; 17215 background: white; 17216 } 17217 17218 /* Custom Scrollbar for Conversations List */ 17219 .chat-conversations-list::-webkit-scrollbar { 17220 width: 6px; 17221 } 17222 17223 .chat-conversations-list::-webkit-scrollbar-track { 17224 background: #f1f1f1; 17225 } 17226 17227 .chat-conversations-list::-webkit-scrollbar-thumb { 17228 background: #F28C1A; 17229 border-radius: 3px; 17230 } 17231 17232 .chat-conversations-list::-webkit-scrollbar-thumb:hover { 17233 background: #D97809; 17234 } 17235 17236 /* Firefox */ 17237 .chat-conversations-list { 17238 scrollbar-width: thin; 17239 scrollbar-color: #F28C1A #f1f1f1; 17240 } 17241 17242 .chat-conversation-item { 17243 padding: 12px 16px; 17244 cursor: pointer; 17245 display: flex; 17246 gap: 12px; 17247 align-items: flex-start; 17248 transition: background 0.15s ease; 17249 color: #33371f; 17250 border-bottom: 1px solid #f0f0f0; 17251 } 17252 17253 .chat-conversation-item:hover { 17254 background: #f5f5f5; 17255 } 17256 17257 .chat-conversation-item.active { 17258 background: #FFF5EB; 17259 } 17260 17261 /* WhatsApp-like Avatar */ 17262 .conversation-avatar { 17263 width: 48px; 17264 height: 48px; 17265 border-radius: 50%; 17266 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17267 flex-shrink: 0; 17268 display: flex; 17269 align-items: center; 17270 justify-content: center; 17271 color: white; 17272 font-weight: 600; 17273 font-size: 18px; 17274 } 17275 17276 .conversation-avatar svg { 17277 width: 24px; 17278 height: 24px; 17279 } 17280 17281 /* WhatsApp-like Content */ 17282 .conversation-content { 17283 flex: 1; 17284 min-width: 0; 17285 display: flex; 17286 flex-direction: column; 17287 gap: 4px; 17288 } 17289 17290 .conversation-header { 17291 display: flex; 17292 justify-content: space-between; 17293 align-items: center; 17294 gap: 8px; 17295 } 17296 17297 .conversation-name { 17298 font-weight: 600; 17299 font-size: 15px; 17300 color: #111; 17301 white-space: nowrap; 17302 overflow: hidden; 17303 text-overflow: ellipsis; 17304 } 17305 17306 .conversation-time { 17307 font-size: 12px; 17308 color: #7d8166; 17309 flex-shrink: 0; 17310 } 17311 17312 .conversation-preview { 17313 display: flex; 17314 justify-content: space-between; 17315 align-items: center; 17316 gap: 8px; 17317 } 17318 17319 .conversation-message { 17320 font-size: 14px; 17321 color: #667781; 17322 white-space: nowrap; 17323 overflow: hidden; 17324 text-overflow: ellipsis; 17325 margin: 0; 17326 flex: 1; 17327 } 17328 17329 .conversation-unread { 17330 background: #25D366; 17331 color: white; 17332 border-radius: 12px; 17333 padding: 2px 8px; 17334 font-size: 12px; 17335 font-weight: 600;
17336 min-width: 20px; 17337 text-align: center; 17338 flex-shrink: 0; 17339 } 17340 17341 /* Active item styling */ 17342 .chat-conversation-item.active .conversation-name { 17343 color: #F28C1A; 17344 } 17345 17346 .chat-conversation-item.active .conversation-time { 17347 color: #F28C1A; 17348 } 17349 17350 .chat-new-btn { 17351 margin: 12px; 17352 padding: 12px; 17353 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17354 color: white; 17355 border: none; 17356 border-radius: 8px; 17357 cursor: pointer; 17358 font-weight: 500; 17359 font-size: 14px; 17360 display: flex; 17361 align-items: center; 17362 justify-content: center; 17363 gap: 8px; 17364 transition: all 0.2s; 17365 flex-shrink: 0; 17366 } 17367 17368 .chat-new-btn:hover { 17369 transform: translateY(-1px); 17370 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17371 } 17372 17373 .chat-conversation-title { 17374 flex: 1; 17375 font-weight: 600; 17376 font-size: 16px; 17377 color: #1f2937; 17378 } 17379 17380 .chat-messages-container { 17381 flex: 1; 17382 overflow-y: auto; 17383 padding: 16px; 17384 display: flex; 17385 flex-direction: column; 17386 gap: 8px; 17387 min-height: 0; 17388 background: #E5DDD5; 17389 } 17390 17391 /* Custom Scrollbar for Messages Container */ 17392 .chat-messages-container::-webkit-scrollbar { 17393 width: 6px; 17394 } 17395 17396 .chat-messages-container::-webkit-scrollbar-track { 17397 background: rgba(0, 0, 0, 0.05); 17398 } 17399 17400 .chat-messages-container::-webkit-scrollbar-thumb { 17401 background: rgba(242, 140, 26, 0.6); 17402 border-radius: 3px; 17403 } 17404 17405 .chat-messages-container::-webkit-scrollbar-thumb:hover { 17406 background: #F28C1A; 17407 } 17408 17409 /* Firefox */ 17410 .chat-messages-container { 17411 scrollbar-width: thin; 17412 scrollbar-color: rgba(242, 140, 26, 0.6) rgba(0, 0, 0, 0.05); 17413 } 17414 17415 /* WhatsApp Group-like Messages */ 17416 .chat-message { 17417 display: flex; 17418 margin-bottom: 4px; 17419 animation: fadeIn 0.2s ease; 17420 } 17421 17422 @keyframes fadeIn { 17423 from { opacity: 0; transform: translateY(10px); } 17424 to { opacity: 1; transform: translateY(0); } 17425 } 17426 17427 .chat-message.mine { 17428 justify-content: flex-end; 17429 } 17430 17431 .chat-message-bubble { 17432 max-width: 80%; 17433 padding: 6px 8px 8px 10px; 17434 border-radius: 8px; 17435 position: relative; 17436 box-shadow: 0 1px 0.5px rgba(0,0,0,0.13); 17437 } 17438 17439 /* Bubble from others (Admin/CS) */ 17440 .chat-message:not(.mine) .chat-message-bubble { 17441 background: #FFFFFF; 17442 border-radius: 0 8px 8px 8px; 17443 } 17444 17445 /* Bubble tail for others */ 17446 .chat-message:not(.mine) .chat-message-bubble::before { 17447 content: ''; 17448 position: absolute; 17449 top: 0; 17450 left: -8px; 17451 width: 0; 17452 height: 0; 17453 border-style: solid; 17454 border-width: 0 8px 13px 0; 17455 border-color: transparent #FFFFFF transparent transparent; 17456 } 17457 17458 /* Bubble from user (mine) */ 17459 .chat-message.mine .chat-message-bubble { 17460 background: #e4f5d0; /* Light blue for user messages */ 17461 border-radius: 8px 0 8px 8px; 17462 } 17463 17464 /* Bubble tail for mine */ 17465 .chat-message.mine .chat-message-bubble::before { 17466 content: ''; 17467 position: absolute; 17468 top: 0; 17469 right: -8px; 17470 width: 0; 17471 height: 0; 17472 border-style: solid; 17473 border-width: 0 0 13px 8px; 17474 border-color: transparent transparent transparent #eaf5d0; 17475 } 17476 17477 .message-sender { 17478 font-size: 12.5px; 17479 font-weight: 600; 17480 color: #F28C1A !important; 17481 margin-bottom: 4px;
17482 } 17483 17484 .chat-message.mine .message-sender { 17485 display: none; 17486 } 17487 17488 /* Message text */ 17489 .message-text { 17490 font-size: 14px; 17491 line-height: 19px; 17492 color: #ffffff !important; 17493 word-wrap: break-word; 17494 white-space: pre-wrap; 17495 margin-bottom: 4px; 17496 } 17497 17498 /* Message metadata (time + status) */ 17499 .message-meta { 17500 display: flex; 17501 align-items: center; 17502 justify-content: flex-end; 17503 gap: 4px; 17504 margin-top: 2px; 17505 } 17506 17507 .message-time { 17508 font-size: 11px; 17509 color: #728166 !important; 17510 line-height: 15px; 17511 } 17512 17513 /* Use multiple selectors to override global styles */ 17514 .chat-message-bubble .message-status, 17515 .message-meta .message-status, 17516 .chat-message .message-status { 17517 width: 16px !important; 17518 height: 16px !important; 17519 min-width: 16px !important; 17520 min-height: 16px !important; 17521 max-width: 16px !important; 17522 max-height: 16px !important; 17523 flex-shrink: 0; 17524 color: #728166 !important; 17525 fill: currentColor !important; 17526 } 17527 17528 .chat-message:not(.mine) .message-status { 17529 display: none; 17530 } 17531 17532 /* Loading dots animation */ 17533 @keyframes loadingDots { 17534 0%, 20% { content: '.'; } 17535 40% { content: '..'; } 17536 60%, 100% { content: '...'; } 17537 } 17538 17539 .message-status.loading::after { 17540 content: '...'; 17541 animation: loadingDots 1.5s infinite; 17542 font-size: 16px; 17543 line-height: 16px; 17544 } 17545 17546 .message-status.loading path { 17547 display: none; 17548 } 17549 17550 .message-status.failed { 17551 color: #ef4444 !important; 17552 cursor: pointer; 17553 } 17554 17555 .message-status.sent { 17556 color: #667781 !important; 17557 } 17558 17559 .chat-new-form { 17560 flex: 1; 17561 overflow-y: auto; 17562 padding: 16px; 17563 display: flex; 17564 flex-direction: column; 17565 } 17566 17567 .chat-new-form form { 17568 display: flex; 17569 flex-direction: column; 17570 gap: 16px; 17571 flex: 1; 17572 } 17573 17574 .chat-new-form .form-group { 17575 display: flex; 17576 flex-direction: column; 17577 gap: 6px; 17578 } 17579 17580 .chat-new-form label { 17581 font-size: 14px; 17582 font-weight: 500; 17583 color: #374151; 17584 } 17585 17586 .chat-new-form .form-control { 17587 width: 100%; 17588 padding: 10px 12px; 17589 border: 1px solid #d1d5db; 17590 border-radius: 8px; 17591 font-size: 14px; 17592 font-family: inherit; 17593 transition: border-color 0.2s; 17594 box-sizing: border-box; 17595 } 17596 17597 .chat-new-form .form-control:focus { 17598 outline: none; 17599 border-color: #F28C1A; 17600 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17601 } 17602 17603 .chat-new-form textarea.form-control { 17604 resize: vertical; 17605 min-height: 100px; 17606 } 17607 17608 .chat-new-form .btn { 17609 padding: 12px 20px; 17610 border: none; 17611 border-radius: 8px; 17612 font-size: 15px; 17613 font-weight: 500; 17614 cursor: pointer; 17615 transition: all 0.2s; 17616 text-align: center; 17617 } 17618 17619 .chat-new-form .btn-primary { 17620 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17621 color: white; 17622 } 17623 17624 .chat-new-form .btn-primary:hover { 17625 transform: translateY(-1px); 17626 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17627 } 17628 17629 .chat-new-form .btn-primary:active { 17630 transform: scale(0.98); 17631 } 17632 17633 .chat-new-form .btn-block { 17634 width: 100%; 17635 margin-top: auto; 17636 } 17637 17638 .chat-message-form { 17639 padding: 12px; 17640 border-top: 1px solid #e5e7eb; 17641 display: flex; 17642 flex-wrap: wrap; 17643 gap: 8px; 17644 align-items: flex-end; 17645 flex-shrink: 0; 17646 background: white; 17647 } 17648 17649 .chat-product-card { 17650 flex: 1 1 100%; 17651 max-width: 100%; 17652 display: flex; 17653 gap: 12px; 17654 padding: 12px; 17655 background: #f3f4f6; 17656 border-radius: 8px; 17657 margin-bottom: 4px;
17658 border: 1px solid #e5e7eb; 17659 } 17660 17661 .chat-product-card img { 17662 width: 60px; 17663 height: 60px; 17664 object-fit: cover; 17665 border-radius: 6px; 17666 flex-shrink: 0; 17667 } 17668 17669 .chat-product-card-content { 17670 flex: 1; 17671 min-width: 0; 17672 } 17673 17674 .chat-product-card-name { 17675 font-weight: 500; 17676 font-size: 14px; 17677 color: #1f2937; 17678 margin-bottom: 4px; 17679 white-space: nowrap; 17680 overflow: hidden; 17681 text-overflow: ellipsis; 17682 } 17683 17684 .chat-product-card-variant { 17685 font-size: 12px; 17686 color: #74806b; 17687 margin-bottom: 4px; 17688 } 17689 17690 .chat-product-card-price { 17691 font-size: 13px; 17692 color: #8bea66; 17693 font-weight: 600; 17694 } 17695 17696 .chat-product-card-remove { 17697 align-self: flex-start; 17698 background: none; 17699 border: none; 17700 color: #9ca3af; 17701 cursor: pointer; 17702 padding: 4px; 17703 font-size: 18px; 17704 line-height: 1; 17705 transition: color 0.2s; 17706 } 17707 17708 .chat-product-card-remove:hover { 17709 color: #ef4444; 17710 } 17711 17712 /* Product card in messages (read-only display) */ 17713 .message-product-card { 17714 display: flex; 17715 gap: 10px; 17716 padding: 10px; 17717 background: rgba(255, 255, 255, 0.5); 17718 border-radius: 8px; 17719 margin-bottom: 6px; 17720 border: 1px solid rgba(0, 0, 0, 0.08); 17721 align-items: center; 17722 } 17723 17724 .chat-message.mine .message-product-card { 17725 background: rgba(255, 255, 255, 0.7); 17726 } 17727 17728 .message-product-card img { 17729 width: 50px; 17730 height: 50px; 17731 object-fit: cover; 17732 border-radius: 6px; 17733 flex-shrink: 0; 17734 } 17735 17736 .message-product-info { 17737 flex: 1; 17738 min-width: 0; 17739 } 17740 17741 .message-product-name { 17742 font-weight: 500; 17743 font-size: 13px; 17744 color: #33371f; 17745 margin-bottom: 2px; 17746 overflow: hidden; 17747 text-overflow: ellipsis; 17748 display: -webkit-box; 17749 -webkit-line-clamp: 2; 17750 -webkit-box-orient: vertical; 17751 line-height: 1.3; 17752 } 17753 17754 .message-product-variant { 17755 font-size: 11px; 17756 color: #78806b; 17757 margin-bottom: 2px; 17758 } 17759 17760 .message-product-price { 17761 font-size: 12px; 17762 color: #c7ea66; 17763 font-weight: 600; 17764 } 17765 17766 .message-product-link { 17767 flex-shrink: 0; 17768 width: 32px; 17769 height: 32px; 17770 display: flex; 17771 align-items: center; 17772 justify-content: center; 17773 background: rgba(102, 126, 234, 0.1); 17774 border-radius: 6px; 17775 color: #c7ea66; 17776 text-decoration: none; 17777 transition: all 0.2s; 17778 } 17779 17780 .message-product-link:hover { 17781 background: #b7ea66; 17782 color: white; 17783 } 17784 17785 .message-product-link svg { 17786 width: 16px; 17787 height: 16px; 17788 } 17789 17790 .chat-message-input { 17791 flex: 1; 17792 border: 1px solid #d1d5db; 17793 border-radius: 20px; 17794 padding: 10px 16px; 17795 resize: none; 17796 max-height: 100px; 17797 font-family: inherit; 17798 font-size: 14px; 17799 line-height: 1.5; 17800 box-sizing: border-box; 17801 overflow-y: hidden; 17802 } 17803 17804 .chat-message-input:focus { 17805 outline: none; 17806 border-color: #F28C1A; 17807 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17808 } 17809 17810 .chat-send-btn { 17811 width: 40px; 17812 height: 40px; 17813 border-radius: 50%; 17814 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17815 border: none; 17816 color: white; 17817 cursor: pointer; 17818 flex-shrink: 0; 17819 display: flex; 17820 align-items: center; 17821 justify-content: center; 17822 transition: all 0.2s; 17823 } 17824 17825 .chat-send-btn:hover { 17826 transform: scale(1.05); 17827 box-shadow: 0 2px 8px rgba(242, 140, 26, 0.3); 17828 } 17829 17830 .chat-send-btn:active { 17831 transform: scale(0.95); 17832 } 17833 17834 .chat-loading { 17835 text-align: center; 17836 padding: 20px; 17837 color: #6b7280; 17838 } 17839 17840 /* Date Separator (WhatsApp-style) */ 17841 .date-separator { 17842 display: flex; 17843 align-items: center; 17844 gap: 12px; 17845 margin: 24px 0 16px; 17846 opacity: 0; 17847 animation: fadeInSeparator 0.3s ease forwards; 17848 } 17849 17850 @keyframes fadeInSeparator { 17851 from { 17852 opacity: 0; 17853 transform: translateY(-10px); 17854 } 17855 to { 17856 opacity: 1;
17857 transform: translateY(0); 17858 } 17859 } 17860 17861 .date-separator-line { 17862 flex: 1; 17863 height: 1px; 17864 background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1) 50%, transparent); 17865 } 17866 17867 .date-separator-text { 17868 flex-shrink: 0; 17869 padding: 4px 12px; 17870 background: #e9ecef; 17871 color: #6c757d; 17872 font-size: 11px; 17873 font-weight: 600; 17874 text-transform: uppercase; 17875 letter-spacing: 0.5px; 17876 border-radius: 12px; 17877 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 17878 } 17879 17880 /* Mobile responsive */ 17881 @media (max-width: 640px) { 17882 .chat-popup { 17883 position: fixed !important; /* Override absolute positioning */ 17884 bottom: 45px !important; /* Above bottom nav */ 17885 right: 0 !important; 17886 left: 0 !important; 17887 width: 100% !important; 17888 max-width: 100% !important; 17889 /* Use dvh (dynamic viewport height) for browser UI awareness */ 17890 height: calc(100dvh - 45px) !important; 17891 max-height: calc(100dvh - 45px) !important; 17892 /* Fallback for browsers that don't support dvh */ 17893 height: calc(var(--viewport-height, 100vh) - 45px) !important; 17894 max-height: calc(var(--viewport-height, 100vh) - 45px) !important; 17895 border-radius: 0 !important; 17896 z-index: 9999 !important; 17897 } 17898 17899 .chat-popup-header { 17900 border-radius: 0; 17901 } 17902 17903 .chat-messages-container, 17904 .chat-conversations-list { 17905 /* Adjust for mobile keyboard using dynamic viewport */ 17906 max-height: calc(100dvh - 190px); 17907 /* Fallback for browsers that don't support dvh */ 17908 max-height: calc(var(--viewport-height, 100vh) - 190px); 17909 } 17910 } 17911 </style> 17912 <!-- Chat Widget Button --> 17913 <div id="chat-widget" class="chat-widget" 17914 data-user-id="" 17915 data-is-logged-in="false" 17916 data-csrf-token="df5d776748b1ed8c8dd0d987bae99b1ee3dea6c6dcbe0eedb1bea766c56d569a"> 17917 <button id="chat-toggle-btn" class="chat-toggle-btn" aria-label="Toggle chat"> 17918 <svg class="chat-icon" width="28" height="28" viewBox="0 0 640 640" fill="currentColor"> 17919 <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"/> 17920 </svg> 17921 <span id="chat-unread-badge" class="chat-unread-badge" style="display: none;">0</span> 17922 </button> 17923 17924 <div id="chat-popup" class="chat-popup" style="display: none;"> 17925 <div class="chat-popup-header"> 17926 <div class="chat-header-title"> 17927 <h3>Bantuan</h3> 17928 <span id="chatOperatingHours" class="chat-operating-hours" style="display: none;"></span> 17929 </div> 17930 <div class="chat-header-actions"> 17931 <button id="chatNotifToggle" class="btn-notif-toggle" data-status="not-supported" title="Push notification" style="display: none;"> 17932 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 17933 <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"/> 17934 </svg> 17935 </button> 17936 <button id="chat-close-btn" class="chat-close-btn" aria-label="Close chat">
17936Ã</button> 17937 </div> 17938 </div> 17939 17940 <!-- Notification Info Bar (conditional) --> 17941 <div class="chat-notif-info-bar" id="chatNotifInfoBar" style="display:none;"> 17942 <svg width="14" height="14" viewBox="0 0 24 24" fill="#ffc107"> 17943 <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"/> 17944 </svg> 17945 <span class="small">Aktifkan notifikasi untuk pemberitahuan balasan</span> 17946 <button class="btn-dismiss" onclick="ChatNotification.dismissInfo()">â</button> 17947 </div> 17948 17949 <div class="chat-popup-body"> 17950 <!-- Conversations List View --> 17951 <div id="chat-list-view" class="chat-view"> 17952 <div id="chat-conversations-list" class="chat-conversations-list"> 17953 <div class="chat-loading">Memuat...</div> 17954 <!-- Conversation Item Structure (rendered by JS): 17955 <div class="chat-conversation-item"> 17956 <div class="conversation-avatar"> 17957 <svg>...</svg> 17958 </div> 17959 <div class="conversation-content"> 17960 <div class="conversation-header"> 17961 <span class="conversation-name">Admin Support</span> 17962 <span class="conversation-time">10:30</span> 17963 </div> 17964 <div class="conversation-preview"> 17965 <p class="conversation-message">Terima kasih telah menghubungi kami...</p> 17966 <span class="conversation-unread">2</span> 17967 </div> 17968 </div> 17969 </div> 17970 --> 17971 </div> 17972 <button id="chat-new-btn" class="chat-new-btn"> 17973 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17974 <line x1="12" y1="5" x2="12" y2="19"></line> 17975 <line x1="5" y1="12" x2="19" y2="12"></line> 17976 </svg> 17977 Percakapan Baru 17978 </button> 17979 </div> 17980 17981 <!-- Chat Messages View --> 17982 <div id="chat-messages-view" class="chat-view" style="display: none;"> 17983 <div id="chat-messages-container" class="chat-messages-container"> 17984 <div class="chat-loading">Memuat pesan...</div> 17985 <!-- Message Structure (rendered by JS): 17986 <div class="chat-message"> (or .chat-message.mine) 17987 <div class="chat-message-bubble"> 17988 <div class="message-sender">Admin Support</div> (only for non-mine) 17989 <div class="message-text">Halo, ada yang bisa dibantu?</div> 17990 <div class="message-meta"> 17991 <span class="message-time">10:30</span> 17992 <svg class="message-status">...</svg> (only for mine) 17993 </div> 17994 </div> 17995 </div> 17996 --> 17997 </div> 17998 17999 <form id="chat-message-form" class="chat-message-form"> 18000 <input type="hidden" id="chat-current-conversation-id" value=""> 18001 <textarea 18002 id="chat-message-input" 18003 class="chat-message-input" 18004 placeholder="Ketik pesan..." 18005 rows="1" 18006 maxlength="5000" 18007 ></textarea> 18008 <button type="submit" class="chat-send-btn" aria-label="Send message"> 18009 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18010 <line x1="22" y1="2" x2="11" y2="
1801013"></line> 18011 <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 18012 </svg> 18013 </button> 18014 </form> 18015 </div> 18016 18017 <!-- New Chat View --> 18018 <div id="chat-new-view" class="chat-view" style="display: none;"> 18019 <div class="chat-messages-header"> 18020 <button id="chat-new-back-btn" class="chat-back-btn" aria-label="Back to list"> 18021 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18022 <path d="M19 12H5M12 19l-7-7 7-7"/> 18023 </svg> 18024 </button> 18025 <div class="chat-conversation-title">Percakapan Baru</div> 18026 </div> 18027 18028 <div class="chat-new-form"> 18029 <form id="chat-start-form"> 18030 <div class="form-group"> 18031 <label for="chat-new-subject">Subjek</label> 18032 <input 18033 type="text" 18034 id="chat-new-subject" 18035 class="form-control" 18036 placeholder="Bantuan Pelanggan" 18037 maxlength="255" 18038 > 18039 </div> 18040 <div class="form-group"> 18041 <label for="chat-new-message">Pesan</label> 18042 <textarea 18043 id="chat-new-message" 18044 class="form-control" 18045 rows="4" 18046 placeholder="Tulis pertanyaan Anda..." 18047 required 18048 maxlength="5000" 18049 ></textarea> 18050 </div> 18051 <button type="submit" class="btn btn-primary btn-block">Kirim</button> 18052 </form> 18053 </div> 18054 </div> 18055 </div> 18056 </div> 18057 </div> 18058 18059 <!-- Login Required Modal --> 18060 <div class="modal-overlay" id="chatLoginModal"> 18061 <div class="modal-dialog"> 18062 <div class="modal-header"> 18063 <h3 class="modal-title"> 18064 <div class="modal-icon info"> 18065 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18066 <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"/> 18067 </svg> 18068 </div> 18069 Login Diperlukan 18070 </h3> 18071 </div> 18072 <div class="modal-body"> 18073 <p class="modal-message">Anda harus login terlebih dahulu untuk menggunakan fitur chat bantuan pelanggan.</p> 18074 <div class="modal-actions"> 18075 <button class="modal-btn modal-btn-secondary" id="chatLoginCancel">Batal</button> 18076 <button class="modal-btn modal-btn-primary" id="chatLoginConfirm">Login Sekarang</button> 18077 </div> 18078 </div> 18079 </div> 18080 </div> 18081
18081<script src="https://plazait.co.id/assets/js/chat-widget.js" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script>
18081<script data-inlined="chat-notification.js"> 18082/** 18083 * Customer Chat Push Notification Manager 18084 * Handles browser push notification subscription for customers 18085 * Version: 1.0.0 18086 * Date: 2026-01-19 18087 */ 18088 18089const ChatNotification = { 18090 // State 18091 status: 'not-supported', // not-supported, not-subscribed, subscribed, blocked 18092 subscription: null, 18093 vapidPublicKey: null, // Will be loaded from server 18094 18095 /** 18096 * Initialize notification manager 18097 */ 18098 async init() { 18099 // Check if user is logged in (only for logged-in users) 18100 if (!this.isUserLoggedIn()) { 18101 this.updateUI('not-supported'); 18102 return; 18103 } 18104 18105 // Check browser support 18106 if (!this.isSupported()) { 18107 this.updateUI('not-supported'); 18108 return; 18109 } 18110 18111 // Load VAPID key from server 18112 await this.loadVapidKey(); 18113 18114 if (!this.vapidPublicKey) { 18115 this.updateUI('not-supported'); 18116 return; 18117 } 18118 18119 // Register service worker if needed 18120 await this.registerServiceWorker(); 18121 18122 // Check current permission 18123 const permission = Notification.permission; 18124 18125 if (permission === 'denied') { 18126 this.updateUI('blocked'); 18127 } else if (permission === 'granted') { 18128 // Check if already subscribed 18129 const isSubscribed = await this.checkSubscriptionStatus(); 18130 this.updateUI(isSubscribed ? 'subscribed' : 'not-subscribed'); 18131 } else { 18132 // Default: not subscribed yet 18133 this.updateUI('not-subscribed'); 18134 this.showInfoBar(); 18135 } 18136 }, 18137 18138 /** 18139 * Check if user is logged in 18140 */ 18141 isUserLoggedIn() { 18142 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 18143 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 18144 }, 18145 18146 /** 18147 * Check if browser supports push notifications 18148 */ 18149 isSupported() { 18150 return ( 18151 'serviceWorker' in navigator && 18152 'PushManager' in window && 18153 'Notification' in window 18154 ); 18155 }, 18156 18157 /** 18158 * Load VAPID public key from server 18159 */ 18160 async loadVapidKey() { 18161 try { 18162 const response = await fetch('/api/chat/notifications/vapid_key', { 18163 method: 'GET', 18164 headers: { 18165 'Accept': 'application/json' 18166 } 18167 }); 18168 18169 if (!response.ok) { 18170 return false; 18171 } 18172 18173 const data = await response.json(); 18174 if (data.success && data.data?.public_key) { 18175 this.vapidPublicKey = data.data.public_key; 18176 return true; 18177 } 18178 18179 return false; 18180 18181 } catch (error) { 18182 console.error('[ChatNotification] Error loading VAPID key:', error); 18183 return false; 18184 } 18185 }, 18186 18187 /** 18188 * Register service worker 18189 */ 18190 async registerServiceWorker() { 18191 if (!('serviceWorker' in navigator)) return null; 18192 18193 try { 18194 const registration = await navigator.serviceWorker.register('/sw-push.js', { 18195 scope: '/' 18196 }); 18197 return registration; 18198 } catch (error) { 18199 return null; 18200 } 18201 }, 18202 18203 /** 18204 * Check subscription status from server 18205 */ 18206 async checkSubscriptionStatus() { 18207 try { 18208 const response = await fetch('/api/chat/notifications/status', { 18209 method: 'GET', 18210 credentials: 'include', 18211 headers: { 18212 'Accept': 'application/json' 18213 } 18214 }); 18215 18216 if (!response.ok) { 18217 return false; 18218 } 18219 18220 const data = await response.json(); 18221 return data.data?.subscribed || false; 18222 18223 } catch (error) { 18224 console.error('[ChatNotification] Error checking subscription:', error); 18225 return false; 18226 } 18227 }, 18228 18229 /** 18230 * Update UI based on status 18231 */ 18232 updateUI(status) { 18233 this.status = status; 18234 const btn = document.getElementById('chatNotifToggle'); 18235 18236 if (!btn) { 18237 // Retry after a short delay (button might not be in DOM yet) 18238 setTimeout(() => this.updateUI(status), 100); 18239 return; 18240 } 18241 18242 btn.setAttribute('data-status', status); 18243 18244 // Show/hide button based on support 18245 if (status === 'not-supported') { 18246 btn.style.display = 'none'; 18247 } else { 18248 btn.style.display = 'block'; 18249 } 18250 18251 // Update SVG icon (bell vs bell-slash) 18252 const svg = btn.querySelector('svg'); 18253 if (svg) { 18254 if (status === 'subscribed') { 18255 // Bell icon (active) 18256 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"/>'; 18257 } else { 18258 // Bell slash icon (inactive/blocked) 18259 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"/>'; 18260 } 18261 } 18262 18263 // Update tooltip 18264 const tooltips = { 18265 'not-subscribed': 'Klik untuk aktifkan notifikasi', 18266 'subscribed': 'Notifikasi aktif', 18267 'blocked': 'Notifikasi diblokir'
18268 }; 18269 18270 if (tooltips[status]) { 18271 btn.title = tooltips[status]; 18272 } 18273 18274 // Hide info bar if subscribed or blocked 18275 if (status === 'subscribed' || status === 'blocked') { 18276 this.hideInfoBar(); 18277 } 18278 }, 18279 18280 /** 18281 * Show info bar (if not dismissed) 18282 */ 18283 showInfoBar() { 18284 if (localStorage.getItem('chat_notif_info_dismissed')) { 18285 return; 18286 } 18287 18288 const infoBar = document.getElementById('chatNotifInfoBar'); 18289 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18290 18291 if (infoBar) { 18292 infoBar.style.display = 'flex'; 18293 18294 // Bind dismiss button here so it's always attached when bar is visible 18295 const btn = infoBar.querySelector('#chatNotifDismissBtn'); 18296 if (btn && !btn._dismissBound) { 18297 btn._dismissBound = true; 18298 btn.addEventListener('click', () => { 18299 infoBar.style.display = 'none'; 18300 localStorage.setItem('chat_notif_info_dismissed', '1'); 18301 if (popupBody) popupBody.classList.remove('has-info-bar'); 18302 }); 18303 } 18304 } 18305 18306 if (popupBody) { 18307 popupBody.classList.add('has-info-bar'); 18308 } 18309 }, 18310 18311 /** 18312 * Hide info bar 18313 */ 18314 hideInfoBar() { 18315 const infoBar = document.getElementById('chatNotifInfoBar'); 18316 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18317 18318 if (infoBar) { 18319 infoBar.style.display = 'none'; 18320 } 18321 18322 if (popupBody) { 18323 popupBody.classList.remove('has-info-bar'); 18324 } 18325 }, 18326 18327 /** 18328 * Dismiss info bar permanently 18329 */ 18330 dismissInfo() { 18331 this.hideInfoBar(); 18332 localStorage.setItem('chat_notif_info_dismissed', '1'); 18333 }, 18334 18335 /** 18336 * Handle bell icon click 18337 */ 18338 async handleToggleClick() { 18339 if (this.status === 'not-subscribed') { 18340 await this.subscribe(); 18341 } else if (this.status === 'subscribed') { 18342 await this.unsubscribe(); 18343 } else if (this.status === 'blocked') { 18344 this.showUnblockHelp(); 18345 } 18346 }, 18347 18348 /** 18349 * Subscribe to push notifications 18350 */ 18351 async subscribe() { 18352 try { 18353 // Request permission 18354 const permission = await Notification.requestPermission(); 18355 18356 if (permission === 'denied') { 18357 this.updateUI('blocked'); 18358 this.showToast('Notifikasi diblokir', 'error'); 18359 return; 18360 } 18361 18362 if (permission !== 'granted') { 18363 return; 18364 } 18365 18366 // Get service worker registration 18367 const registration = await navigator.serviceWorker.ready; 18368 18369 // Subscribe to push 18370 const subscription = await registration.pushManager.subscribe({ 18371 userVisibleOnly: true, 18372 applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) 18373 }); 18374 18375 // Send subscription to server 18376 const response = await fetch('/api/chat/notifications/subscribe', { 18377 method: 'POST', 18378 credentials: 'include', 18379 headers: { 18380 'Content-Type': 'application/json', 18381 'Accept': 'application/json' 18382 }, 18383 body: JSON.stringify(subscription.toJSON()) 18384 }); 18385 18386 if (!response.ok) { 18387 throw new Error('Failed to save subscription'); 18388 } 18389 18390 const result = await response.json(); 18391 18392 this.subscription = subscription; 18393 this.updateUI('subscribed'); 18394 this.hideInfoBar(); 18395 this.showToast('Notifikasi berhasil diaktifkan!', 'success'); 18396 18397 // Send test notification 18398 setTimeout(() => this.sendTestNotification(), 1000); 18399 18400 } catch (error) { 18401 console.error('[ChatNotification] Subscribe error:', error); 18402 this.showToast('Gagal mengaktifkan notifikasi', 'error'); 18403 } 18404 }, 18405 18406 /** 18407 * Unsubscribe from push notifications 18408 */ 18409 async unsubscribe() { 18410 try { 18411 // Confirm first 18412 if (!confirm('Nonaktifkan notifikasi chat?')) { 18413 return; 18414 } 18415 18416 // Get current subscription 18417 const registration = await navigator.serviceWorker.ready; 18418 const subscription = await registration.pushManager.getSubscription(); 18419 18420 if (subscription) { 18421 // Unsubscribe from browser 18422 await subscription.unsubscribe(); 18423 18424 // Tell server 18425 await fetch('/api/chat/notifications/unsubscribe', { 18426 method: 'POST', 18427 credentials: 'include', 18428 headers: { 18429 'Content-Type': 'application/json', 18430 'Accept': 'application/json' 18431 }, 18432 body: JSON.stringify(subscription.toJSON()) 18433 }); 18434 } 18435 18436 this.subscription = null; 18437 this.updateUI('not-subscribed'); 18438 this.showInfoBar(); 18439 this.showToast('Notifikasi dinonaktifkan', 'info'); 18440 18441 } catch (error) { 18442 console.error('[ChatNotification] Unsubscribe error:', error); 18443 this.showToast('Gagal menonaktifkan notifikasi', 'error'); 18444 } 18445 }, 18446 18447 /** 18448 * Send test notification 18449 */ 18450 async sendTestNotification() { 18451 try { 18452 const response = await fetch('/api/chat/notifications/send_test', { 18453 method: 'POST', 18454 credentials: 'include', 18455 headers: { 18456 'Accept': 'application/json' 18457 } 18458 }); 18459 18460 if (response.ok) { 18461 // Test notification sent successfully 18462 } 18463 } catch (error) { 18464 // Test notification failed silently 18465 } 18466 }, 18467 18468 /** 18469 * Show unblock help modal/message 18470 */ 18471 showUnblockHelp() { 18472 const message = ` 18473 Notifikasi diblokir oleh browser.\n\n 18474 Untuk mengaktifkan:\n 18475 1. Klik ikon gembok/info di address bar\n 18476 2. Cari "Notifications" atau "Notifikasi"\n 18477 3. Ubah menjadi "Allow" atau "Izinkan"\n 18478 4. Refresh halaman ini 18479 `.trim().replace(/\s+/g, ' '); 18480 18481 alert(message); 18482 }, 18483 18484 /** 18485 * Show toast notification 18486 */ 18487 showToast(message, type = 'info') { 18488 // Simple toast implementation 18489 const toast = document.createElement('div'); 18490 toast.className = `chat-toast chat-toast-${type}`; 18491 toast.textContent = message; 18492 toast.style.cssText = ` 18493 position: fixed; 18494 bottom: 80px; 18495 right: 20px; 18496 background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; 18497 color: white; 18498 padding: 12px 20px; 18499 border-radius: 4px; 18500 box-shadow: 0 2px 8px rgba(0,0,0,0.2); 18501 z-index: 10000; 18502 animation: slideInUp 0.3s ease; 18503 `; 18504 18505 document.body.appendChild(toast); 18506 18507 setTimeout(() => { 18508 toast.style.animation = 'slideOutDown 0.3s ease'; 18509 setTimeout(() => toast.remove(), 300); 18510 }, 3000); 18511 }, 18512 18513 /** 18514 * Convert VAPID key to Uint8Array 18515 */ 18516 urlBase64ToUint8Array(base64String) { 18517 const padding = '='.repeat((4 - base64String.length % 4) % 4); 18518 const base64 = (base64String + padding) 18519 .replace(/\-/g, '+') 18520 .replace(/_/g, '/'); 18521 18522 const rawData = window.atob(base64); 18523 const outputArray = new Uint8Array(rawData.length); 18524 18525 for (let i = 0; i < rawData.length; ++i) { 18526 outputArray[i] = rawData.charCodeAt(i); 18527 } 18528 return outputArray; 18529 }, 18530 18531 /** 18532 * Check if push notification is currently active 18533 * @returns {Promise<boolean>} 18534 */ 18535 async isPushActive() { 18536 try { 18537 // Check browser subscription 18538 if (!('serviceWorker' in navigator)) return false; 18539 18540 const registration = await navigator.serviceWorker.getRegistration(); 18541 if (!registration) return false; 18542 18543 const subscription = await registration.pushManager.getSubscription(); 18544 if (!subscription) return false; 18545 18546 // Also check server status 18547 const serverStatus = await this.checkSubscriptionStatus();
18548 18549 return serverStatus; 18550 } catch (error) { 18551 return false; 18552 } 18553 }, 18554 18555 /** 18556 * Show logout confirmation if push notification is active 18557 * Call this before logout action 18558 * @returns {Promise<boolean>} true if user confirms logout, false if cancelled 18559 */ 18560 async confirmLogoutWithPushNotification() { 18561 const isActive = await this.isPushActive(); 18562 18563 if (!isActive) { 18564 // No active push, proceed with logout 18565 return true; 18566 } 18567 18568 // Show modal confirmation using modal.css 18569 return new Promise((resolve) => { 18570 // Create modal HTML 18571 const modalHTML = ` 18572 <div class="modal-overlay" id="logoutConfirmModal"> 18573 <div class="modal-dialog"> 18574 <div class="modal-header"> 18575 <h3 class="modal-title"> 18576 <span class="modal-icon warning">â ï¸</span> 18577 Konfirmasi Logout 18578 </h3> 18579 </div> 18580 <div class="modal-body"> 18581 <p class="modal-message"> 18582 Anda memiliki <strong>notifikasi push yang aktif</strong>.<br><br> 18583 Setelah logout, Anda tidak akan menerima notifikasi chat lagi di perangkat ini.<br><br> 18584 Apakah Anda yakin ingin melanjutkan logout? 18585 </p> 18586 </div> 18587 <div class="modal-actions" style="padding: 0 20px 20px;"> 18588 <button class="modal-btn modal-btn-secondary" id="logoutCancelBtn"> 18589 Batal 18590 </button> 18591 <button class="modal-btn modal-btn-danger" id="logoutConfirmBtn"> 18592 Lanjutkan Logout 18593 </button> 18594 </div> 18595 </div> 18596 </div> 18597 `; 18598 18599 // Append to body 18600 const modalContainer = document.createElement('div'); 18601 modalContainer.innerHTML = modalHTML; 18602 document.body.appendChild(modalContainer.firstElementChild); 18603 18604 const modal = document.getElementById('logoutConfirmModal'); 18605 const confirmBtn = document.getElementById('logoutConfirmBtn'); 18606 const cancelBtn = document.getElementById('logoutCancelBtn'); 18607 18608 // Show modal with animation 18609 requestAnimationFrame(() => { 18610 modal.classList.add('show'); 18611 }); 18612 18613 // Handle confirm 18614 const handleConfirm = () => { 18615 cleanup(); 18616 resolve(true); 18617 }; 18618 18619 // Handle cancel 18620 const handleCancel = () => { 18621 cleanup(); 18622 resolve(false); 18623 }; 18624 18625 // Cleanup function 18626 const cleanup = () => { 18627 modal.classList.remove('show'); 18628 setTimeout(() => { 18629 modal.remove(); 18630 }, 200); 18631 confirmBtn.removeEventListener('click', handleConfirm); 18632 cancelBtn.removeEventListener('click', handleCancel); 18633 modal.removeEventListener('click', handleBackdropClick); 18634 }; 18635 18636 // Handle backdrop click 18637 const handleBackdropClick = (e) => { 18638 if (e.target === modal) { 18639 handleCancel(); 18640 } 18641 }; 18642 18643 // Add event listeners 18644 confirmBtn.addEventListener('click', handleConfirm); 18645 cancelBtn.addEventListener('click', handleCancel); 18646 modal.addEventListener('click', handleBackdropClick); 18647 }); 18648 } 18649}; 18650 18651// Add CSS animations 18652const chatNotifStyle = document.createElement('style'); 18653chatNotifStyle.textContent = ` 18654 @keyframes slideInUp { 18655 from { 18656 transform: translateY(100%); 18657 opacity: 0; 18658 } 18659 to { 18660 transform: translateY(0); 18661 opacity: 1;
18662 } 18663 } 18664 18665 @keyframes slideOutDown { 18666 from { 18667 transform: translateY(0); 18668 opacity: 1; 18669 } 18670 to { 18671 transform: translateY(100%); 18672 opacity: 0; 18673 } 18674 } 18675`; 18676document.head.appendChild(chatNotifStyle); 18677 18678// Auto-initialize when DOM is ready 18679if (document.readyState === 'loading') { 18680 document.addEventListener('DOMContentLoaded', () => ChatNotification.init()); 18681} else { 18682 ChatNotification.init(); 18683} 18684 18685// Expose to window for access from other scripts 18686window.ChatNotification = ChatNotification; 18687 18688</script>
18688 18689 <!-- Footer Collapse Toggle Script --> 18690
18690<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 18691 document.addEventListener('DOMContentLoaded', function() { 18692 const ftTop = document.getElementById('ftTop'); 18693 const expandBtn = document.getElementById('ftExpandBtn'); 18694 18695 if (ftTop && expandBtn) { 18696 const expandText = expandBtn.querySelector('.expand-text'); 18697 18698 expandBtn.addEventListener('click', function() { 18699 ftTop.classList.toggle('expanded'); 18700 const isExpanded = ftTop.classList.contains('expanded'); 18701 if (expandText) { 18702 expandText.textContent = isExpanded ? 'Sembunyikan' : 'Selengkapnya'; 18703 } 18704 }); 18705 } 18706 }); 18707 </script>
18707 18708 </footer> 18709 18710<!-- Compare Sidebar Overlay --> 18711<div class="compare-overlay" id="compareOverlay"></div> 18712 18713<!-- Floating Compare Badge (Sticky) --> 18714<button class="compare-floating-badge" id="compareFloatingBadge" aria-label="Lihat produk yang dibandingkan" style="display:none;"> 18715 <svg class="cfb-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 18716 <path d="M9.01 14H2v2h7.01v3L13 15l-3.99-4v3zm5.98-1v-3H22V8h-7.01V5L11 9l3.99 4z"/> 18717 </svg> 18718 <span class="cfb-text">Bandingkan</span> 18719 <span class="cfb-count" id="cfbCount">0</span> 18720</button> 18721 18722<!-- AI Comparison Modal --> 18723<div class="ai-compare-modal" id="aiCompareModal"> 18724 <div class="modal-backdrop"></div> 18725 <div class="modal-container"> 18726 <div class="modal-header"> 18727 <h3 class="modal-title"> 18728 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 18729 <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"/> 18730 </svg> 18731 Analisis AI: Perbandingan Produk 18732 </h3> 18733 <button class="modal-close" id="aiCompareModalClose" aria-label="Tutup"> 18734 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18735 <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"/> 18736 </svg> 18737 </button> 18738 </div> 18739 18740 <div class="modal-content"> 18741 <!-- Loading State --> 18742 <div class="ai-loading" id="aiLoading"> 18743 <div class="loading-animation"> 18744 <div class="typing-indicator"> 18745 <span></span> 18746 <span></span> 18747 <span></span> 18748 </div> 18749 <p>AI sedang menganalisis produk...</p> 18750 <small>Proses ini membutuhkan waktu 10-30 detik</small> 18751 </div> 18752 </div> 18753 18754 <!-- Error State --> 18755 <div class="ai-error" id="aiError" hidden> 18756 <div class="error-icon"> 18757 <svg width="48" height="48" viewBox="0 0 24 24" fill="#ef4444"> 18758 <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"/> 18759 </svg> 18760 </div> 18761 <h4>Analisis AI Gagal</h4> 18762 <p id="aiErrorMessage">Terjadi kesalahan saat memproses analisis.</p> 18763 <button class="btn btn-outline-primary" id="aiRetry">Coba Lagi</button> 18764 </div> 18765 18766 <!-- Analysis Results --> 18767 <div class="ai-results" id="aiResults" hidden> 18768 <!-- Summary Section --> 18769 <div class="ai-summary-card"> 18770 <h4> 18771 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18772 <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"/> 18773 </svg> 18774 Ringkasan Analisis 18775 </h4> 18776 <p id="aiSummary" class="summary-text"></p> 18777 <div class="winner-badge" id="winnerBadge"> 18778 <strong>Rekomendasi Terbaik:</strong> <span id="winnerProduct"></span> 18779 <small id="winnerReason"></small> 18780 </div> 18781 </div> 18782 18783 <!-- Comparison Table --> 18784 <div class="ai-comparison-table"> 18785 <h4> 18786 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18787 <path d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"/> 18788 </svg> 18789 Perbandingan Detail 18790 </h4> 18791 <div id="comparisonTable" class="comparison-grid"></div> 18792 </div> 18793 18794 <!-- Detailed Analysis --> 18795 <div class="ai-detailed-analysis"> 18796 <h4> 18797 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18798 <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"/> 18799 </svg> 18800 Analisis Mendalam 18801 </h4> 18802 <div id="detailedAnalysis" class="products-analysis"></div> 18803 </div> 18804 18805 <!-- Recommendations --> 18806 <div class="ai-recommendations"> 18807 <h4> 18808 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18809 <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"/> 18810 </svg> 18811 Rekomendasi Berdasarkan Kebutuhan 18812 </h4> 18813 <div class="recommendations-grid"> 18814 <div class="recommendation-card"> 18815 <h5>ð° Budget Conscious</h5> 18816 <p id="budgetRecommendation"></p> 18817 </div> 18818 <div class="recommendation-card"> 18819 <h5>â¡ Performance Focused</h5> 18820 <p id="performanceRecommendation"></p> 18821 </div> 18822 <div class="recommendation-card"> 18823 <h5>âï¸ Balanced Choice</h5> 18824 <p id="balancedRecommendation"></p> 18825 </div> 18826 </div> 18827 </div> 18828 18829 <!-- Buying Tips --> 18830 <div class="ai-tips"> 18831 <h4> 18832 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18833 <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
18833M5,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"/> 18834 </svg> 18835 Tips Pembelian 18836 </h4> 18837 <ul id="buyingTips" class="tips-list"></ul> 18838 </div> 18839 </div> 18840 </div> 18841 </div> 18842</div> 18843 18844<!-- Cart Login Modal --> 18845<div class="modal-overlay" id="cartLoginModal"> 18846 <div class="modal-dialog"> 18847 <div class="modal-header"> 18848 <h3 class="modal-title"> 18849 <div class="modal-icon info"> 18850 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18851 <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"/> 18852 </svg> 18853 </div> 18854 Login untuk Melanjutkan 18855 </h3> 18856 </div> 18857 <div class="modal-body"> 18858 <p class="modal-message" style="margin-bottom: 16px;">Silakan login atau daftar untuk mengakses keranjang belanja dan menyelesaikan pesanan Anda.</p> 18859 <div style="background: #f8fafc; border-radius: 8px; padding: 12px; margin-bottom: 20px;"> 18860 <p style="font-size: 14px; color: #646947; margin: 0; line-height: 1.6;"> 18861 <strong style="color: #2a2a0f;">Keuntungan dengan akun:</strong><br> 18862 â Simpan keranjang belanja Anda<br> 18863 â Lacak status pesanan secara real-time<br> 18864 â Checkout lebih cepat dengan data tersimpan<br> 18865 â Dapatkan notifikasi promo & diskon eksklusif 18866 </p> 18867 </div> 18868 <div class="modal-actions"> 18869 <button class="modal-btn modal-btn-secondary" id="cartLoginCancel">Batal</button> 18870 <button class="modal-btn modal-btn-primary" id="cartLoginConfirm" style="background: #ff6b35; border-color: #ff6b35;">Login</button> 18871 <button class="modal-btn modal-btn-primary" id="cartRegisterBtn" style="background: #f7931e; border-color: #f7931e;">Daftar</button> 18872 </div> 18873 </div> 18874 </div> 18875</div> 18876 18877
18878<script data-inlined="app.js"> 18879(function initLiveSearch(){ 18880 function setup() { 18881 const searchInput = document.getElementById('searchInput'); 18882 const suggest = document.getElementById('searchSuggest'); 18883 const searchBtn = document.getElementById('searchBtn'); 18884 const aiToggleBtn = document.getElementById('aiToggleBtn'); 18885 18886 if (!searchInput || !suggest || !aiToggleBtn) { 18887 console.log('[LiveSearch] Elemen belum ada, retry...'); 18888 return false; 18889 } 18890 18891 let debounceTimer = null; 18892 let lastQuery = ''; 18893 let currentIndex = -1; 18894 let currentAbortController = null; 18895 let isAIModeEnabled = false; // AI mode OFF by default 18896 let isAIProcessing = false; // Flag to prevent closing suggest box during AI processing 18897 18898 // Trending cache 18899 let trendingCache = null; 18900 let trendingLoading = false; 18901 18902 // Trending products cache 18903 let trendingProductsCache = null; 18904 let trendingProductsLoading = false; 18905 18906 const MIN_LEN = 2; 18907 const DEBOUNCE = 300; 18908 const AI_TIMEOUT = 15000; // 15 seconds timeout for AI endpoint 18909 18910 // Load AI mode preference from localStorage 18911 const savedAIMode = localStorage.getItem('plazait_ai_search_mode'); 18912 if (savedAIMode === 'enabled') { 18913 isAIModeEnabled = false; 18914 aiToggleBtn.classList.remove('active'); 18915 } 18916 18917 // Update placeholder based on AI mode 18918 function updatePlaceholder() { 18919 if (isAIModeEnabled) { 18920 searchInput.placeholder = 'ð¤ Coba: "laptop HP core i3 untuk kerja" atau "tinta epson l3210"'; 18921 } else { 18922 // Static placeholder for typing animation 18923 searchInput.placeholder = ''; 18924 } 18925 } 18926 18927 // Typing animation for placeholder 18928 let typingTimeout = null; 18929 let currentProductIndex = 0; 18930 let currentCharIndex = 0; 18931 let isTyping = false; 18932 let isDeleting = false; 18933 18934 const popularProducts = [ 18935 'Oppo A6', 18936 'Asus Ryzen 3', 18937 'Starlink V4', 18938 'HP Victus', 18939 'Lenovo RTX 5060', 18940 'ROG Strix', 18941 'iPhone 13', 18942 'Xpad 20', 18943 'Xiaomi 14', 18944 'Epson L3210', 18945 'Realme C85', 18946 'Brother T720', 18947 'Mouse Logitech' 18948 ]; 18949 18950 function typeWriter() { 18951 if (isAIModeEnabled || document.activeElement === searchInput) { 18952 return; // Skip typing if AI mode is on or input is focused 18953 } 18954 18955 const currentProduct = popularProducts[currentProductIndex]; 18956 18957 if (!isDeleting) { 18958 // Typing forward 18959 if (currentCharIndex < currentProduct.length) { 18960 searchInput.placeholder = currentProduct.substring(0, currentCharIndex + 1); 18961 currentCharIndex++; 18962 typingTimeout = setTimeout(typeWriter, 100); // Typing speed 18963 } else { 18964 // Finished typing, wait before deleting 18965 typingTimeout = setTimeout(() => { 18966 isDeleting = true; 18967 typeWriter(); 18968 }, 2000); // Pause before deleting 18969 } 18970 } else { 18971 // Deleting backward 18972 if (currentCharIndex > 0) { 18973 searchInput.placeholder = currentProduct.substring(0, currentCharIndex - 1); 18974 currentCharIndex--; 18975 typingTimeout = setTimeout(typeWriter, 50); // Deleting speed (faster) 18976 } else { 18977 // Finished deleting, move to next product 18978 isDeleting = false; 18979 currentProductIndex = (currentProductIndex + 1) % popularProducts.length; 18980 typingTimeout = setTimeout(typeWriter, 500); // Pause before typing next 18981 } 18982 } 18983 } 18984 18985 function startTypingAnimation() { 18986 if (!isAIModeEnabled && !typingTimeout) { 18987 currentCharIndex = 0; 18988 isDeleting = false; 18989 typeWriter(); 18990 } 18991 } 18992 18993 function stopTypingAnimation() { 18994 if (typingTimeout) { 18995 clearTimeout(typingTimeout); 18996 typingTimeout = null; 18997 } 18998 } 18999 19000 // Set initial placeholder 19001 updatePlaceholder(); 19002 19003 // Start typing animation for normal mode 19004 if (!isAIModeEnabled) { 19005 startTypingAnimation(); 19006 } 19007 19008 function esc(str){ 19009 if (str === null || str === undefined) return ''; 19010 return String(str) 19011 .replace(/&/g,'&') 19012 .replace(/</g,'<') 19013 .replace(/>/g,'>') 19014 .replace(/"/g,'"') 19015 .replace(/'/g,'''); 19016 } 19017 19018 function clearSuggest(){ 19019 suggest.innerHTML = ''; 19020 suggest.hidden = true; 19021 suggest.classList.remove('open', 'trending-mode'); 19022 currentIndex = -1; 19023 } 19024 19025 // ==================== TRENDING FUNCTIONS ==================== 19026 19027 async function fetchTrending() { 19028 if (trendingCache) return trendingCache; 19029 if (trendingLoading) return null; 19030 19031 trendingLoading = true; 19032
19033 try { 19034 const res = await fetch('/api/trending_searches.php?timeframe=7d&limit=8'); 19035 const data = await res.json(); 19036 19037 if (data.ok && Array.isArray(data.data)) { 19038 trendingCache = data.data; 19039 console.log('[Trending] Loaded', trendingCache.length, 'items'); 19040 return trendingCache; 19041 } 19042 } catch (err) { 19043 console.error('[Trending] Fetch error:', err); 19044 } finally { 19045 trendingLoading = false; 19046 } 19047 19048 return null; 19049 } 19050 19051 async function fetchTrendingProducts() { 19052 if (trendingProductsCache) return trendingProductsCache; 19053 if (trendingProductsLoading) return null; 19054 19055 trendingProductsLoading = true; 19056 19057 try { 19058 const res = await fetch('/api/trending_products.php?timeframe=7d&limit=6'); 19059 const data = await res.json(); 19060 19061 if (data.ok && Array.isArray(data.data)) { 19062 trendingProductsCache = data.data; 19063 console.log('[Trending Products] Loaded', trendingProductsCache.length, 'items'); 19064 return trendingProductsCache; 19065 } 19066 } catch (err) { 19067 console.error('[Trending Products] Fetch error:', err); 19068 } finally { 19069 trendingProductsLoading = false; 19070 } 19071 19072 return null; 19073 } 19074 19075 function renderTrendingSuggestions(trends, products) { 19076 if ((!trends || trends.length === 0) && (!products || products.length === 0)) { 19077 clearSuggest(); 19078 return; 19079 } 19080 19081 let html = `<div class="trending-container">`; 19082 19083 // Left column: Trending searches 19084 if (trends && trends.length > 0) { 19085 html += ` 19086 <div class="trending-column trending-searches"> 19087 <div class="trending-header"> 19088 <span class="trending-icon">ð¥</span> 19089 <span class="trending-title">Pencarian Trending</span> 19090 </div> 19091 `; 19092 19093 html += trends.map((t, idx) => { 19094 const categoryHint = t.category_hint ? `<span class="trending-category">${esc(t.category_hint)}</span>` : ''; 19095 return ` 19096 <div class="trending-item" data-query="${esc(t.query)}" data-index="${idx}"> 19097 <span class="trending-rank"> 19098 <img src="/assets/img/uptrend.image" alt="trending" width="16" height="16" style="vertical-align: middle;"> 19099 </span> 19100 <span class="trending-query">${esc(t.query)}</span> 19101 ${categoryHint} 19102 </div> 19103 `; 19104 }).join(''); 19105 19106 html += `</div>`; 19107 } 19108 19109 // Right column: Trending products 19110 if (products && products.length > 0) { 19111 html += ` 19112 <div class="trending-column trending-products"> 19113 <div class="trending-header"> 19114 <span class="trending-icon">â</span> 19115 <span class="trending-title">Produk Populer</span> 19116 </div> 19117 `; 19118 19119 html += products.map((p, idx) => { 19120 return ` 19121 <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}"> 19122 <img src="${esc(p.image)}" alt="${esc(p.name)}" class="trending-product-thumb"> 19123 <div class="trending-product-info"> 19124 <div class="trending-product-name">${esc(p.name)}</div> 19125 <div class="trending-product-price">${esc(p.price_label)}</div> 19126 </div> 19127 </div> 19128 `; 19129 }).join(''); 19130 19131 html += `</div>`; 19132 } 19133 19134 html += `</div>`; 19135 19136 suggest.innerHTML = html; 19137 suggest.hidden = false; 19138 suggest.classList.add('open', 'trending-mode'); 19139 suggest.style.display = 'block'; 19140 19141 console.log('[Trending] Rendered', trends?.length || 0, 'searches and', products?.length || 0, 'products'); 19142 } 19143 19144 function trackSearchClick(query, productId) { 19145 fetch('/api/track_search_click.php', { 19146 method: 'POST', 19147 headers: { 'Content-Type': 'application/json' }, 19148 body: JSON.stringify({ 19149 query: query, 19150 product_id: productId,
19151 click_type: 'suggestion' 19152 }) 19153 }).catch(() => {}); // silent fail 19154 } 19155 19156 function trackTrendingClick(query) { 19157 fetch('/api/track_search_click.php', { 19158 method: 'POST', 19159 headers: { 'Content-Type': 'application/json' }, 19160 body: JSON.stringify({ 19161 query: query, 19162 click_type: 'trending' 19163 }) 19164 }).catch(() => {}); 19165 } 19166 19167 function trackTrendingProductClick(productId, query) { 19168 fetch('/api/track_search_click.php', { 19169 method: 'POST', 19170 headers: { 'Content-Type': 'application/json' }, 19171 body: JSON.stringify({ 19172 query: query || 'trending_product', 19173 product_id: productId, 19174 click_type: 'trending_product' 19175 }) 19176 }).catch(() => {}); 19177 } 19178 19179 function trackSearchSubmit(query) { 19180 fetch('/api/track_search_click.php', { 19181 method: 'POST', 19182 headers: { 'Content-Type': 'application/json' }, 19183 body: JSON.stringify({ 19184 query: query, 19185 click_type: 'submit' 19186 }) 19187 }).catch(() => {}); 19188 } 19189 19190 function logFinalSearch(query) { 19191 // Log the final search to analytics (not during typing) 19192 fetch('/api/search_products.php?q=' + encodeURIComponent(query) + '&log=1', { 19193 headers: { 'Accept': 'application/json' } 19194 }).catch(() => {}); 19195 } 19196 19197 // ==================== END TRENDING FUNCTIONS ==================== 19198 19199 function clearSuggest(){ 19200 suggest.innerHTML = ''; 19201 suggest.hidden = true; 19202 suggest.classList.remove('open', 'trending-mode'); 19203 currentIndex = -1; 19204 } 19205 19206 // AI Toggle Handler 19207 aiToggleBtn.addEventListener('click', (e) => { 19208 e.preventDefault(); 19209 e.stopPropagation(); 19210 19211 isAIModeEnabled = !isAIModeEnabled; 19212 19213 if (isAIModeEnabled) { 19214 aiToggleBtn.classList.add('active'); 19215 localStorage.setItem('plazait_ai_search_mode', 'enabled'); 19216 updatePlaceholder(); // Update placeholder text 19217 stopTypingAnimation(); // Stop typing animation in AI mode 19218 19219 // Show AI prompt if there's a query 19220 const query = searchInput.value.trim(); 19221 if (query.length >= MIN_LEN) { 19222 showAISearchPrompt(query); 19223 } else { 19224 showAIActivationMessage(); 19225 } 19226 } else { 19227 aiToggleBtn.classList.remove('active'); 19228 localStorage.setItem('plazait_ai_search_mode', 'disabled'); 19229 updatePlaceholder(); // Update placeholder text 19230 startTypingAnimation(); // Start typing animation in normal mode 19231 clearSuggest(); 19232 19233 // Switch to normal search if there's a query 19234 const query = searchInput.value.trim(); 19235 if (query.length >= MIN_LEN) { 19236 performSearch(query); 19237 } 19238 } 19239 }); 19240 19241 function showAIActivationMessage() { 19242 suggest.innerHTML = ` 19243 <div class="ai-progress-container"> 19244 <div class="ai-progress-header"> 19245 <div class="ai-progress-icon">â¨</div> 19246 <div class="ai-progress-text"> 19247 <div class="ai-progress-title">AI Search Mode Activated!</div> 19248 <div class="ai-progress-subtitle">Ketik query Anda untuk pencarian cerdas</div> 19249 </div> 19250 </div> 19251 </div> 19252 `; 19253 suggest.hidden = false; 19254 suggest.classList.add('open'); 19255 suggest.style.display = 'block'; 19256 } 19257 19258 function renderAIInsights(insights){ 19259 if (!insights) return ''; 19260 19261 // Generate AI summary via backend endpoint 19262 const summary = insights.ai_summary || generateAISummary(insights); 19263 19264 if (!summary) return ''; 19265 19266 return ` 19267 <div class="ai-result-banner"> 19268 <div class="ai-result-icon">â¨</div> 19269 <div class="ai-result-text"> 19270 <div class="ai-result-title">AI Assistant</div> 19271 <div class="ai-result-description">${summary}</div> 19272 </div> 19273 </div> 19274 `; 19275 } 19276 19277 function generateAISummary(insights) { 19278 // Fallback client-side summary generation if backend doesn't provide ai_summary 19279 const productName = insights.product_name || ''; 19280 const categoryPath = insights.category_path || ''; 19281 const priceRange = insights.price_range || {}; 19282 const intent = insights.intent || ''; 19283 19284 let summary = ''; 19285 19286 // Parse category for natural display 19287 let categoryDisplay = ''; 19288 if (categoryPath) { 19289 const categoryParts = categoryPath.split('/');
19290 categoryDisplay = categoryParts[categoryParts.length - 1] || categoryParts[categoryParts.length - 2] || categoryPath; 19291 } 19292 19293 // Intent-based opening 19294 if (intent === 'cek_stok') { 19295 summary = `Berikut ketersediaan stok untuk <strong>${esc(productName)}</strong>. `; 19296 } else if (intent === 'rekomendasi_produk') { 19297 summary = `Berikut rekomendasi produk <strong>${esc(categoryDisplay || productName)}</strong> yang kami pilihkan untuk Anda. `; 19298 } else { 19299 // Default: cari_harga or general 19300 if (productName && categoryDisplay) { 19301 summary = `Kami menemukan <strong>${esc(productName)}</strong> di kategori ${esc(categoryDisplay)}`; 19302 } else if (productName) { 19303 summary = `Kami menemukan produk <strong>${esc(productName)}</strong>`; 19304 } else if (categoryDisplay) { 19305 summary = `Kami menemukan produk ${esc(categoryDisplay)}`; 19306 } 19307 19308 // Add price context 19309 if (priceRange.min > 0 && priceRange.max > 0) { 19310 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19311 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19312 summary += ` dengan kisaran harga ${minPrice} - ${maxPrice}`; 19313 } else if (priceRange.min > 0) { 19314 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19315 summary += ` dengan harga mulai ${minPrice}`; 19316 } else if (priceRange.max > 0) { 19317 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19318 summary += ` dengan harga maksimal ${maxPrice}`; 19319 } 19320 19321 summary += ' yang sesuai dengan kebutuhan Anda.'; 19322 } 19323 19324 return summary || esc(insights.explanation || ''); 19325 } 19326 19327 function showAIProgressSteps() { 19328 return ` 19329 <div class="ai-progress-container"> 19330 <div class="ai-progress-header"> 19331 <div class="ai-progress-icon">ð¤</div> 19332 <div class="ai-progress-text"> 19333 <div class="ai-progress-title">AI sedang menganalisis...</div> 19334 <div class="ai-progress-subtitle">Memproses query Anda dengan kecerdasan buatan</div> 19335 </div> 19336 </div> 19337 <div class="ai-progress-steps"> 19338 <div class="ai-progress-step active"> 19339 <div class="ai-step-icon">1</div> 19340 <div class="ai-step-text">Memahami maksud pencarian...</div> 19341 </div> 19342 <div class="ai-progress-step pending"> 19343 <div class="ai-step-icon">2</div> 19344 <div class="ai-step-text">Mengekstrak parameter pencarian...</div> 19345 </div> 19346 <div class="ai-progress-step pending"> 19347 <div class="ai-step-icon">3</div> 19348 <div class="ai-step-text">Mencari produk yang relevan...</div> 19349 </div> 19350 </div> 19351 </div> 19352 `; 19353 } 19354 19355 function updateAIProgressStep(step) { 19356 const steps = suggest.querySelectorAll('.ai-progress-step'); 19357 19358 steps.forEach((stepEl, idx) => { 19359 const icon = stepEl.querySelector('.ai-step-icon'); 19360 19361 if (idx < step) { 19362 stepEl.classList.remove('active', 'pending'); 19363 stepEl.classList.add('completed'); 19364 icon.textContent = 'â'; 19365 } else if (idx === step) { 19366 stepEl.classList.remove('pending', 'completed'); 19367 stepEl.classList.add('active'); 19368 } 19369 }); 19370 } 19371
19372 function renderSuggest(items, aiInsights){ 19373 if (!items.length){ 19374 clearSuggest(); 19375 return; 19376 } 19377 19378 let html = ''; 19379 19380 // Show AI insights if available 19381 if (aiInsights && aiInsights.explanation) { 19382 html += renderAIInsights(aiInsights); 19383 } 19384 19385 // Section header for products with hint 19386 html += ` 19387 <div class="suggest-section-header"> 19388 <div class="suggest-header-main"> 19389 <span class="suggest-header-icon">ð¦</span> 19390 <span class="suggest-header-text">Produk yang ditemukan (${items.length})</span> 19391 </div> 19392 <div class="suggest-header-hint">Tekan <kbd>Enter</kbd> untuk lihat semua hasil</div> 19393 </div> 19394 `; 19395 19396 // Render product items 19397 html += items.map((it, idx)=>{ 19398 // Normalisasi & fallback 19399 const id = it.id != null ? it.id : ''; 19400 const name = esc(it.name); 19401 const slug = esc(it.slug != null ? it.slug : it.id); 19402 const img = esc(it.image || '/assets/img/no-image.png'); 19403 const price = esc(it.price_label || ''); 19404 19405 const url = '/product/' + encodeURIComponent(slug); 19406 19407 return ` 19408 <div class="suggest-item" data-index="${idx}" data-url="${url}" data-id="${esc(id)}"> 19409 <img src="${img}" alt="${name}" class="suggest-thumb"> 19410 <div class="suggest-meta"> 19411 <div class="suggest-name">${name}</div> 19412 <div class="suggest-price">${price}</div> 19413 </div> 19414 </div> 19415 `; 19416 }).join(''); 19417 19418 // Footer hint for more results - only show if 10 items (means there might be more) 19419 if (items.length >= 10) { 19420 html += ` 19421 <div class="suggest-footer-hint"> 19422 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: -3px; margin-right: 6px;"> 19423 <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"/> 19424 </svg> 19425 <span>Lihat hasil lengkap dengan menekan <kbd>Enter</kbd> atau <a href="#" class="suggest-footer-link" id="footerSearchLink">klik di sini</a></span> 19426 </div> 19427 `; 19428 } 19429 19430 suggest.innerHTML = html; 19431 suggest.hidden = false; 19432 suggest.classList.add('open'); 19433 suggest.style.display = 'block'; 19434 suggest.style.visibility = 'visible'; 19435 suggest.style.opacity = '1'; 19436 } 19437 19438 function fetchRemote(q, shouldLog = false){ 19439 // Cancel previous request if any 19440 if (currentAbortController) { 19441 currentAbortController.abort(); 19442 } 19443 currentAbortController = new AbortController(); 19444 19445 // Always use standard search for normal mode 19446 // Only add log=1 parameter when shouldLog is true (intentional search) 19447 const logParam = shouldLog ? '&log=1' : ''; 19448 const url = '/api/search_products.php?q=' + encodeURIComponent(q) + logParam; 19449 const fetchOptions = { 19450 headers: {'Accept':'application/json'}, 19451 signal: currentAbortController.signal 19452 }; 19453 19454 return fetch(url, fetchOptions) 19455 .then(r=>r.json()) 19456 .then(j=>{ 19457 if (j.ok && Array.isArray(j.data)) { 19458 return j; 19459 } 19460 return { ok: false, data: [] }; 19461 }) 19462 .catch(err=>{ 19463 if (err.name === 'AbortError') { 19464 return { ok: false, data: [] }; 19465 } 19466 return { ok:false, data:[] }; 19467 }); 19468 } 19469 19470 function showNoResults(q, aiInsights) { 19471 suggest.innerHTML = ` 19472 <div style="padding: 32px 24px; text-align: center; color: #64748b;"> 19473 <div style="font-size: 56px; margin-bottom: 16px; opacity: 0.6;">ð</div> 19474 <div style="font-size: 16px; font-weight: 600; color: #334155; margin-bottom: 8px;"> 19475 Tidak ada hasil untuk "${esc(q)}" 19476 </div> 19477 ${aiInsights && aiInsights.explanation ? ` 19478 <div style="font-size: 13px; margin: 16px auto; padding: 12px 16px; background: #f1f5f9; border-radius: 8px; max-width: 400px; color: #475569;"> 19479 <strong style="color: #667eea;">ð¤ AI memahami:</strong><br> 19480 ${esc(aiInsights.explanation)} 19481 </div> 19482 ` : ''} 19483 <div style="font-size: 13px; margin-top: 16px; opacity: 0.8;"> 19484 Coba kata kunci lain atau hubungi kami untuk bantuan 19485 </div> 19486 </div> 19487 `; 19488 suggest.hidden = false; 19489 suggest.classList.add('open'); 19490 suggest.style.display = 'block'; 19491 } 19492 19493 function showErrorMessage() { 19494 suggest.innerHTML = ` 19495 <div style="padding: 32px 24px; text-align: center; color: #dc2626;"> 19496 <div style="font-size: 48px; margin-bottom: 12px;">â ï¸</div> 19497 <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;"> 19498 Pencarian tidak tersedia 19499 </div> 19500 <div style="font-size: 12px; opacity: 0.8;">
19501 Silakan coba lagi dalam beberapa saat 19502 </div> 19503 </div> 19504 `; 19505 suggest.hidden = false; 19506 suggest.classList.add('open'); 19507 suggest.style.display = 'block'; 19508 } 19509 19510 function showAIErrorState(q) { 19511 suggest.innerHTML = ` 19512 <div class="ai-progress-container"> 19513 <div class="ai-progress-header"> 19514 <div class="ai-progress-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);">â¡</div> 19515 <div class="ai-progress-text"> 19516 <div class="ai-progress-title">AI sedang memproses...</div> 19517 <div class="ai-progress-subtitle">Menganalisis query: "${esc(q)}"</div> 19518 </div> 19519 </div> 19520 <div style="padding: 16px; margin-top: 12px; background: #fef3c7; border-radius: 8px; border-left: 3px solid #f59e0b;"> 19521 <div style="font-size: 13px; color: #92400e; line-height: 1.6;"> 19522 <strong style="display: block; margin-bottom: 4px;">ð¡ Sedang mencari produk terbaik untuk Anda</strong> 19523 AI kami sedang menganalisis database untuk menemukan hasil yang paling relevan. 19524 Mohon tunggu sebentar... 19525 </div> 19526 </div> 19527 <div style="padding: 12px 16px; margin-top: 12px; text-align: center;"> 19528 <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;"> 19529 ð Refresh & Coba Lagi 19530 </button> 19531 </div> 19532 </div> 19533 `; 19534 suggest.hidden = false; 19535 suggest.classList.add('open'); 19536 suggest.style.display = 'block'; 19537 } 19538 19539 function triggerAISearch(q) { 19540 lastQuery = q; // Update lastQuery to prevent re-triggering 19541 isAIProcessing = true; // Set flag to prevent suggest box from closing 19542 19543 // Show AI progress UI - keep suggest box visible 19544 suggest.innerHTML = showAIProgressSteps(); 19545 suggest.hidden = false; 19546 suggest.classList.add('open'); 19547 suggest.style.display = 'block'; 19548 19549 setTimeout(() => updateAIProgressStep(1), 1000); 19550 setTimeout(() => updateAIProgressStep(2), 2500); 19551 19552 // Cancel previous request if any 19553 if (currentAbortController) { 19554 currentAbortController.abort(); 19555 } 19556 currentAbortController = new AbortController(); 19557 19558 const url = '/api/search_ai.php?q=' + encodeURIComponent(q); 19559 const fetchOptions = { 19560 headers: {'Accept':'application/json'}, 19561 signal: currentAbortController.signal 19562 }; 19563 19564 // Add timeout 19565 const timeoutId = setTimeout(() => { 19566 currentAbortController.abort(); 19567 19568 // Show fallback loading state 19569 suggest.innerHTML = ` 19570 <div class="ai-progress-container"> 19571 <div class="ai-progress-header"> 19572 <div class="ai-progress-icon">â³</div> 19573 <div class="ai-progress-text"> 19574 <div class="ai-progress-title">AI timeout - switching to standard search</div> 19575 <div class="ai-progress-subtitle">Mencari dengan metode tradisional...</div> 19576 </div> 19577 </div> 19578 </div> 19579 `; 19580 19581 // Fallback to traditional search 19582 fetch('/api/search_products.php?q=' + encodeURIComponent(q), { 19583 headers: { 'Accept': 'application/json' } 19584 }) 19585 .then(r => r.json()) 19586 .then(j => { 19587 isAIProcessing = false; // Reset flag 19588 if (j.ok && Array.isArray(j.data) && j.data.length) { 19589 renderSuggest(j.data, null); 19590 } else { 19591 showNoResults(q, null); 19592 } 19593 }) 19594 .catch(() => { 19595 isAIProcessing = false; // Reset flag 19596 showAIErrorState(q); 19597 }); 19598 }, AI_TIMEOUT); 19599 19600 fetch(url, fetchOptions) 19601 .then(r => r.json()) 19602 .then(j => { 19603 clearTimeout(timeoutId); 19604 isAIProcessing = false; // Reset flag when done 19605 19606 if (j.ok && Array.isArray(j.data)) { 19607 if (j.data.length > 0) { 19608 renderSuggest(j.data, j.ai_insights || null); 19609 } else { 19610 showNoResults(q, j.ai_insights || null); 19611 } 19612 } else { 19613 showAIErrorState(q); 19614 } 19615 }) 19616 .catch(err => { 19617 clearTimeout(timeoutId); 19618 isAIProcessing = false; // Reset flag on error 19619 if (err.name !== 'AbortError') { 19620 showAIErrorState(q); 19621 } 19622 }); 19623 } 19624 19625 function performSearch(q){ 19626 if (q.length < MIN_LEN){ 19627 clearSuggest(); 19628 return; 19629 } 19630 if (q === lastQuery) return; 19631 lastQuery = q; 19632 19633 // Normal mode only - AI mode uses triggerAISearch() 19634 fetchRemote(q).then(j=>{ 19635 if (j.ok && Array.isArray(j.data)){ 19636 if (j.data.length > 0) { 19637 renderSuggest(j.data, null); 19638 } else { 19639 showNoResults(q, null); 19640 } 19641 } else { 19642 clearSuggest(); 19643 } 19644 }).catch(() => { 19645 clearSuggest(); 19646 }); 19647 } 19648 19649 function debounce(){ 19650 const q = searchInput.value.trim(); 19651 clearTimeout(debounceTimer); 19652 19653 // If empty, show trending suggestions and products 19654 if (q.length === 0) { 19655 debounceTimer = setTimeout(async () => { 19656 const trends = trendingCache || await fetchTrending(); 19657 const products = trendingProductsCache || await fetchTrendingProducts(); 19658 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19659 renderTrendingSuggestions(trends, products); 19660 } else { 19661 clearSuggest(); 19662 } 19663 }, 100); 19664 return; 19665 } 19666 19667 // For AI mode, just show search prompt (no auto search) 19668 if (isAIModeEnabled && q.length >= MIN_LEN) { 19669 showAISearchPrompt(q); 19670 } else if (!isAIModeEnabled && q.length >= MIN_LEN) { 19671 // Normal mode: auto search with debounce 19672 debounceTimer = setTimeout(()=> performSearch(q), DEBOUNCE); 19673 } else { 19674 clearSuggest(); 19675 } 19676 } 19677 19678 function showAISearchPrompt(q) { 19679 suggest.innerHTML = ` 19680 <div class="ai-search-prompt"> 19681 <div class="ai-prompt-header"> 19682 <div class="ai-prompt-icon">ð¤</div> 19683 <div class="ai-prompt-text"> 19684 <div class="ai-prompt-title">Pencarian AI Siap!</div> 19685 <div class="ai-prompt-subtitle">Klik tombol di bawah untuk mencari dengan AI</div> 19686 </div> 19687 </div> 19688 <div class="ai-prompt-query"> 19689 <div class="ai-query-label">
19689Query Anda:</div> 19690 <div class="ai-query-text">"${esc(q)}"</div> 19691 </div> 19692 <button class="ai-search-trigger-btn" data-query="${esc(q)}"> 19693 <span class="ai-btn-icon">â¨</span> 19694 <span class="ai-btn-text">Cari dengan AI</span> 19695 </button> 19696 <div class="ai-prompt-footer"> 19697 AI akan menganalisis query Anda untuk hasil yang lebih relevan 19698 </div> 19699 </div> 19700 `; 19701 suggest.hidden = false; 19702 suggest.classList.add('open'); 19703 suggest.style.display = 'block'; 19704 } 19705 19706 function moveHighlight(dir){ 19707 if (suggest.hidden) return; 19708 const items = suggest.querySelectorAll('.suggest-item'); 19709 if (!items.length) return; 19710 currentIndex += dir; 19711 if (currentIndex < 0) currentIndex = items.length - 1; 19712 if (currentIndex >= items.length) currentIndex = 0; 19713 items.forEach(it=> it.classList.remove('active')); 19714 const active = items[currentIndex]; 19715 if (active){ 19716 active.classList.add('active'); 19717 active.scrollIntoView({block:'nearest'}); 19718 } 19719 } 19720 19721 function activate(){ 19722 const active = suggest.querySelector('.suggest-item.active'); 19723 if (!active) return; 19724 window.location.href = active.getAttribute('data-url'); 19725 } 19726 19727 searchInput.addEventListener('input', debounce); 19728 19729 // Stop typing animation when focused & load trending if empty 19730 searchInput.addEventListener('focus', async () => { 19731 stopTypingAnimation(); 19732 19733 const q = searchInput.value.trim(); 19734 19735 // If empty, show trending suggestions and products 19736 if (q.length === 0) { 19737 const trends = trendingCache || await fetchTrending(); 19738 const products = trendingProductsCache || await fetchTrendingProducts(); 19739 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19740 renderTrendingSuggestions(trends, products); 19741 } 19742 return; 19743 } 19744 19745 // If AI mode and has query, show AI prompt 19746 if (isAIModeEnabled && q.length >= MIN_LEN && suggest.hidden) { 19747 showAISearchPrompt(q); 19748 } 19749 }); 19750 19751 // Resume typing animation when blurred (if suggest is closed) 19752 searchInput.addEventListener('blur', () => { 19753 // Add delay to allow click events to fire first 19754 setTimeout(() => { 19755 if (!isAIModeEnabled && suggest.hidden && searchInput.value.trim() === '') { 19756 startTypingAnimation(); 19757 } 19758 }, 200); 19759 }); 19760 19761 searchInput.addEventListener('keydown', e=>{ 19762 switch(e.key){ 19763 case 'ArrowDown': moveHighlight(1); e.preventDefault(); break; 19764 case 'ArrowUp': moveHighlight(-1); e.preventDefault(); break; 19765 case 'Enter': 19766 if (!suggest.hidden){ 19767 activate(); 19768 e.preventDefault(); 19769 } else { 19770 // Direct enter without suggestion - track search submit and log final search 19771 const q = searchInput.value.trim(); 19772 if (q.length >= MIN_LEN) { 19773 trackSearchSubmit(q); 19774 logFinalSearch(q); 19775 } 19776 } 19777 break; 19778 case 'Escape': clearSuggest(); break; 19779 } 19780 }); 19781 19782 suggest.addEventListener('click', e=>{ 19783 // Check if footer link clicked 19784 const footerLink = e.target.closest('#footerSearchLink'); 19785 if (footerLink) { 19786 e.preventDefault(); 19787 const q = searchInput.value.trim(); 19788 if (q.length >= MIN_LEN) { 19789 trackSearchSubmit(q); 19790 logFinalSearch(q); 19791 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19792 } 19793 return; 19794 } 19795 19796 // Check if AI search button clicked 19797 const aiBtn = e.target.closest('.ai-search-trigger-btn'); 19798 if (aiBtn) { 19799 const query = aiBtn.getAttribute('data-query'); 19800 if (query) { 19801 triggerAISearch(query); 19802 } 19803 return; 19804 } 19805 19806 // Handle trending item click 19807 const trendingItem = e.target.closest('.trending-item'); 19808 if (trendingItem) { 19809 const query = trendingItem.getAttribute('data-query'); 19810 if (query) { 19811 searchInput.value = query; 19812 trackTrendingClick(query); 19813 logFinalSearch(query); // Log final search when user clicks trending 19814 performSearch(query); 19815 } 19816 return; 19817 } 19818 19819 // Handle trending product click 19820 const trendingProduct = e.target.closest('.trending-product-item'); 19821 if (trendingProduct) { 19822 const productId = trendingProduct.getAttribute('data-product-id'); 19823 const slug = trendingProduct.getAttribute('data-slug'); 19824 const query = trendingProduct.getAttribute('data-query'); 19825 19826 if (productId && slug) {
19827 trackTrendingProductClick(productId, query); 19828 window.location.href = '/product/' + encodeURIComponent(slug); 19829 } 19830 return; 19831 } 19832 19833 // Handle product item click 19834 const it = e.target.closest('.suggest-item'); 19835 if (!it) return; 19836 19837 const productId = it.getAttribute('data-id'); 19838 trackSearchClick(lastQuery, productId); 19839 logFinalSearch(lastQuery); // Log final search when user clicks on suggestion 19840 19841 window.location.href = it.getAttribute('data-url'); 19842 }); 19843 19844 document.addEventListener('click', e=>{ 19845 // Don't close suggest box if AI is processing 19846 if (isAIProcessing) return; 19847 19848 if (e.target === searchInput || e.target === aiToggleBtn || 19849 suggest.contains(e.target) || aiToggleBtn.contains(e.target)) return; 19850 clearSuggest(); 19851 }); 19852 19853 if (searchBtn){ 19854 searchBtn.addEventListener('click', ()=>{ 19855 const q = searchInput.value.trim(); 19856 if (q.length >= MIN_LEN){ 19857 // Track search submit and log final search 19858 trackSearchSubmit(q); 19859 logFinalSearch(q); 19860 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19861 } 19862 }); 19863 } 19864 19865 // Pastikan style dasar muncul 19866 if (!suggest.classList.contains('wired')) { 19867 suggest.classList.add('wired'); 19868 suggest.style.position = 'absolute'; 19869 suggest.style.zIndex = '9999'; 19870 suggest.style.background = '#fff'; 19871 suggest.style.border = '1px solid #ccc'; 19872 suggest.style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)'; 19873 suggest.style.maxHeight = '360px'; 19874 suggest.style.overflowY = 'auto'; 19875 } 19876 19877 return true; 19878 } 19879 19880 if (!setup()) { 19881 if (document.readyState === 'loading') { 19882 document.addEventListener('DOMContentLoaded', setup); 19883 } else { 19884 setTimeout(setup, 80); 19885 } 19886 } 19887})(); 19888</script>
vendor: 2 bytes, line 19888
19888
19889<script data-inlined="site-notice.js"> 19890/** 19891 * Site Notice Banner - Interactive Behavior 19892 * Handles dismiss functionality with localStorage persistence 19893 * Supports dynamic notice IDs for different campaigns 19894 */ 19895 19896(function() { 19897 'use strict'; 19898 19899 const STORAGE_KEY = 'plazait_site_notice_dismissed'; 19900 19901 const banner = document.getElementById('holidayNotice'); 19902 const closeBtn = document.getElementById('holidayNoticeClose'); 19903 19904 if (!banner || !closeBtn) return; 19905 19906 // Get notice ID from data attribute 19907 const noticeId = banner.dataset.noticeId; 19908 if (!noticeId) { 19909 console.warn('Notice ID not found. Banner will not persist dismiss state.'); 19910 } 19911 19912 /** 19913 * Check if banner was previously dismissed 19914 */ 19915 function wasDismissed() { 19916 if (!noticeId) return false; 19917 19918 try { 19919 const dismissed = localStorage.getItem(STORAGE_KEY); 19920 return dismissed === noticeId; 19921 } catch (e) { 19922 console.warn('localStorage not available:', e); 19923 return false; 19924 } 19925 } 19926 19927 /** 19928 * Mark banner as dismissed 19929 */ 19930 function markDismissed() { 19931 if (!noticeId) return; 19932 19933 try { 19934 localStorage.setItem(STORAGE_KEY, noticeId); 19935 } catch (e) { 19936 console.warn('Could not save to localStorage:', e); 19937 } 19938 } 19939 19940 /** 19941 * Hide banner with animation 19942 */ 19943 function hideBanner() { 19944 banner.classList.add('hidden'); 19945 document.body.classList.remove('has-holiday-notice'); 19946 19947 // Remove from DOM after animation 19948 setTimeout(() => { 19949 banner.remove(); 19950 }, 300); 19951 19952 markDismissed(); 19953 19954 // Analytics tracking (optional) 19955 if (window.gtag) { 19956 window.gtag('event', 'banner_dismiss', { 19957 'event_category': 'Site Notice', 19958 'event_label': noticeId || 'unknown', 19959 }); 19960 } 19961 } 19962 19963 /** 19964 * Initialize banner 19965 */ 19966 function init() { 19967 // Check if already dismissed 19968 if (wasDismissed()) { 19969 banner.remove(); 19970 return; 19971 } 19972 19973 // Add body class for layout adjustment 19974 document.body.classList.add('has-holiday-notice'); 19975 19976 // Close button click 19977 closeBtn.addEventListener('click', hideBanner); 19978 19979 // Keyboard support for close button 19980 closeBtn.addEventListener('keydown', (e) => { 19981 if (e.key === 'Enter' || e.key === ' ') { 19982 e.preventDefault(); 19983 hideBanner(); 19984 } 19985 }); 19986 19987 // Optional: Auto-hide after certain time (disabled by default) 19988 // setTimeout(hideBanner, 30000); // 30 seconds 19989 } 19990 19991 // Initialize when DOM is ready 19992 if (document.readyState === 'loading') { 19993 document.addEventListener('DOMContentLoaded', init); 19994 } else { 19995 init(); 19996 } 19997 19998})(); 19999 20000</script>
vendor: 2 bytes, line 20000
20000
20001<script data-inlined="guest-cart.js"> 20002/** 20003 * Guest Cart Manager - localStorage based cart for guest users 20004 * 20005 * Manages shopping cart items in browser localStorage for users who are not logged in. 20006 * Provides seamless transition to database when user logs in. 20007 */ 20008 20009class GuestCart { 20010 constructor() { 20011 this.storageKey = 'plazait_guest_cart'; 20012 this.apiBase = '/api/cart'; 20013 20014 // Initialize cart structure 20015 this.cart = this.loadFromStorage(); 20016 20017 // Bind methods 20018 this.add = this.add.bind(this); 20019 this.remove = this.remove.bind(this); 20020 this.update = this.update.bind(this); 20021 this.clear = this.clear.bind(this); 20022 this.getItems = this.getItems.bind(this); 20023 this.mergeToDatabase = this.mergeToDatabase.bind(this); 20024 } 20025 20026 /** 20027 * Load cart from localStorage 20028 */ 20029 loadFromStorage() { 20030 try { 20031 const stored = localStorage.getItem(this.storageKey); 20032 if (stored) { 20033 const parsed = JSON.parse(stored); 20034 if (parsed && typeof parsed === 'object' && parsed.items) { 20035 return parsed; 20036 } 20037 } 20038 } catch (error) { 20039 // Silent error handling 20040 } 20041 20042 return { 20043 items: {}, 20044 updated_at: Date.now(), 20045 version: '1.0' 20046 }; 20047 } 20048 20049 /** 20050 * Save cart to localStorage 20051 */ 20052 saveToStorage() { 20053 try { 20054 this.cart.updated_at = Date.now(); 20055 localStorage.setItem(this.storageKey, JSON.stringify(this.cart)); 20056 20057 // Dispatch custom event for cart updates 20058 window.dispatchEvent(new CustomEvent('guestCartUpdated', { 20059 detail: { 20060 itemsCount: this.getItemsCount(), 20061 linesCount: this.getLinesCount(), 20062 items: this.cart.items 20063 } 20064 })); 20065 20066 return true; 20067 } catch (error) { 20068 return false; 20069 } 20070 } 20071 20072 /** 20073 * Generate cart item key 20074 */ 20075 generateKey(productId, variantId = 0) { 20076 return `p${productId}v${variantId}`; 20077 } 20078 20079 /** 20080 * Add item to guest cart 20081 */ 20082 add(productId, qty = 1, variantId = 0, meta = {}) { 20083 const key = this.generateKey(productId, variantId); 20084 20085 // Validate inputs 20086 if (!productId || productId <= 0 || !qty || qty <= 0) { 20087 throw new Error('Invalid product ID or quantity'); 20088 } 20089 20090 qty = Math.max(1, Math.min(999, parseInt(qty))); 20091 20092 // Initialize item if not exists 20093 if (!this.cart.items[key]) { 20094 this.cart.items[key] = { 20095 pid: parseInt(productId), 20096 vid: parseInt(variantId) || 0, 20097 qty: 0, 20098 name: meta.name || '', 20099 sku: meta.sku || '', 20100 price: parseInt(meta.price) || 0, 20101 image_url: meta.image_url || '', 20102 product_url: meta.product_url || '/product/' + parseInt(productId), 20103 added_at: Date.now() 20104 }; 20105 } 20106 20107 // Update quantity 20108 this.cart.items[key].qty = Math.max(1, Math.min(999, this.cart.items[key].qty + qty)); 20109 20110 // Update metadata if provided 20111 if (meta.name) this.cart.items[key].name = meta.name; 20112 if (meta.sku) this.cart.items[key].sku = meta.sku; 20113 if (meta.price) this.cart.items[key].price = parseInt(meta.price); 20114 if (meta.image_url) this.cart.items[key].image_url = meta.image_url; 20115 if (meta.product_url) this.cart.items[key].product_url = meta.product_url; 20116 20117 this.saveToStorage(); 20118 20119 return { 20120 key, 20121 item: this.cart.items[key], 20122 itemsCount: this.getItemsCount(), 20123 linesCount: this.getLinesCount() 20124 }; 20125 } 20126 20127 /** 20128 * Remove item from guest cart 20129 */ 20130 remove(productId, variantId = 0) { 20131 const key = this.generateKey(productId, variantId); 20132 20133 if (this.cart.items[key]) { 20134 delete this.cart.items[key]; 20135 this.saveToStorage(); 20136 20137 return true; 20138 } 20139 20140 return false; 20141 } 20142 20143 /** 20144 * Update item quantity 20145 */ 20146 update(productId, qty, variantId = 0) {
20147 const key = this.generateKey(productId, variantId); 20148 20149 if (!this.cart.items[key]) { 20150 return false; 20151 } 20152 20153 qty = parseInt(qty); 20154 if (qty <= 0) { 20155 return this.remove(productId, variantId); 20156 } 20157 20158 this.cart.items[key].qty = Math.max(1, Math.min(999, qty)); 20159 this.saveToStorage(); 20160 20161 return true; 20162 } 20163 20164 /** 20165 * Clear entire cart 20166 */ 20167 clear() { 20168 this.cart = { 20169 items: {}, 20170 updated_at: Date.now(), 20171 version: '1.0' 20172 }; 20173 20174 this.saveToStorage(); 20175 20176 return true; 20177 } 20178 20179 /** 20180 * Get all cart items 20181 */ 20182 getItems() { 20183 return this.cart.items; 20184 } 20185 20186 /** 20187 * Get cart items as array 20188 */ 20189 getItemsArray() { 20190 return Object.values(this.cart.items); 20191 } 20192 20193 /** 20194 * Get total items count (sum of quantities) 20195 */ 20196 getItemsCount() { 20197 return Object.values(this.cart.items).reduce((total, item) => total + (item.qty || 0), 0); 20198 } 20199 20200 /** 20201 * Get lines count (number of different items) 20202 */ 20203 getLinesCount() { 20204 return Object.keys(this.cart.items).length; 20205 } 20206 20207 /** 20208 * Get cart summary 20209 */ 20210 getSummary() { 20211 const itemsCount = this.getItemsCount(); 20212 const linesCount = this.getLinesCount(); 20213 const totalValue = Object.values(this.cart.items).reduce( 20214 (total, item) => total + ((item.qty || 0) * (item.price || 0)), 0 20215 ); 20216 20217 return { 20218 itemsCount, 20219 linesCount, 20220 totalValue, 20221 updatedAt: this.cart.updated_at 20222 }; 20223 } 20224 20225 /** 20226 * Check if cart has items 20227 */ 20228 isEmpty() { 20229 return this.getLinesCount() === 0; 20230 } 20231 20232 /** 20233 * Merge guest cart to database after user login 20234 */ 20235 async mergeToDatabase() { 20236 const items = this.cart.items; 20237 20238 if (Object.keys(items).length === 0) { 20239 return { ok: true, merged_count: 0, message: 'No items to merge' }; 20240 } 20241 20242 try { 20243 // Use new endpoint that handles localStorage format 20244 const response = await fetch(`${this.apiBase}/merge-localStorage.php`, { 20245 method: 'POST', 20246 headers: { 20247 'Content-Type': 'application/json', 20248 }, 20249 credentials: 'same-origin', 20250 body: JSON.stringify({ 20251 items: items // Send localStorage format directly 20252 }) 20253 }); 20254 20255 const result = await response.json(); 20256 20257 if (result.ok) { 20258 // Clear localStorage cart after successful merge 20259 this.clear(); 20260 20261 // Update cart counter 20262 if (window.updateCartCounter) { 20263 try { 20264 await window.updateCartCounter(); 20265 } catch (e) { 20266 // Silent error handling 20267 } 20268 } 20269 20270 // Dispatch merge completion event 20271 window.dispatchEvent(new CustomEvent('guestCartMerged', { 20272 detail: result 20273 })); 20274 20275 return result; 20276 } else { 20277 throw new Error(result.error || 'Failed to merge cart'); 20278 } 20279 20280 } catch (error) { 20281 throw error; 20282 } 20283 } 20284 20285 /** 20286 * Export cart data (for debugging or backup) 20287 */ 20288 export() { 20289 return { 20290 ...this.cart, 20291 summary: this.getSummary() 20292 }; 20293 } 20294 20295 /** 20296 * Import cart data (for restore or testing) 20297 */ 20298 import(cartData) { 20299 if (cartData && typeof cartData === 'object' && cartData.items) { 20300 this.cart = { 20301 items: cartData.items, 20302 updated_at: cartData.updated_at || Date.now(), 20303 version: cartData.version || '1.0' 20304 }; 20305 20306 this.saveToStorage(); 20307 return true; 20308 } 20309 20310 return false; 20311 } 20312} 20313 20314// Create global instance 20315window.guestCart = new GuestCart(); 20316 20317// Auto-merge on page load if user is logged in 20318document.addEventListener('DOMContentLoaded', async function() { 20319 // Multiple ways to check if user is logged in 20320 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20321 document.body.classList.contains('logged-in') || 20322 window.userLoggedIn === true || 20323 document.body.dataset.userLoggedIn === 'true'; 20324 20325 if (isLoggedIn && !window.guestCart.isEmpty()) { 20326 // Small delay to ensure page is fully loaded 20327 setTimeout(async () => { 20328 try { 20329 const result = await window.guestCart.mergeToDatabase(); 20330 } catch (error) { 20331 // Silent error handling 20332 } 20333 }, 500); 20334 } 20335}); 20336 20337// Also listen for login events (if login happens via AJAX) 20338window.addEventListener('userLoggedIn', async function() { 20339 if (!window.guestCart.isEmpty()) { 20340 try { 20341 await window.guestCart.mergeToDatabase(); 20342 } catch (error) { 20343 // Silent error handling 20344 } 20345 } 20346}); 20347</script>
vendor: 2 bytes, line 20347
20347
20348<script data-inlined="cart.js"> 20349(function(){ 20350 function $(s,ctx=document){ return ctx.querySelector(s); } 20351 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 20352 20353 // CSRF token helper 20354 function getCsrfToken() { 20355 const metaTag = document.querySelector('meta[name="csrf-token"]'); 20356 return metaTag ? metaTag.getAttribute('content') : ''; 20357 } 20358 20359 function guessPdMeta(){ 20360 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 20361 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 20362 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 20363 const imgEl = $('#pd-main-img'); 20364 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 20365 return { name, sku, image_url }; // price removed for security 20366 } 20367 20368 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 20369 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 20370 const payload = { 20371 product_id: parseInt(productId||0,10) || 0, 20372 qty: parseInt(qty||1,10) || 1, 20373 variant_id: parseInt(variantId||0,10) || 0, 20374 meta 20375 }; 20376 20377 // Check if user is logged in 20378 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20379 document.body.classList.contains('logged-in') || 20380 window.userLoggedIn === true || 20381 document.body.dataset.userLoggedIn === 'true'; 20382 20383 // Skip localStorage untuk logged user - mereka pakai database 20384 if (!isLoggedIn && window.guestCart) { 20385 try { 20386 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 20387 } catch (e) { 20388 console.warn('Failed to add to localStorage cart:', e); 20389 } 20390 } 20391 20392 const res = await fetch('/api/cart/add.php', { 20393 method: 'POST', 20394 headers: { 20395 'Content-Type':'application/json', 20396 'X-CSRF-Token': getCsrfToken() 20397 }, 20398 credentials: 'same-origin', 20399 body: JSON.stringify({ 20400 ...payload, 20401 csrf_token: getCsrfToken() 20402 }), 20403 }); 20404 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 20405 if (!res.ok || !data.ok) { 20406 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 20407 throw new Error(msg); 20408 } 20409 20410 // Update badge di navbar dengan data dari response 20411 try { 20412 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20413 if (el && data.cart && typeof data.cart.items_count === 'number') { 20414 el.textContent = String(data.cart.items_count); 20415 el.classList.toggle('is-empty', data.cart.items_count <= 0); 20416 } 20417 } catch(_){} 20418 20419 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 20420 try { 20421 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 20422 } catch(_){} 20423 20424 // Update CartBadge dengan data dari response (lebih reliable) 20425 try { 20426 if (window.CartBadge && window.CartBadge.set) { 20427 window.CartBadge.set(data.cart.items_count||0); 20428 } 20429 } catch(_){} 20430 20431 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 20432 setTimeout(() => { 20433 if (window.updateCartCounter) { 20434 window.updateCartCounter(); 20435 } 20436 }, 100); 20437 20438 return data; 20439 } 20440 20441 // Function to update cart counter from server 20442 async function updateCartCounter(){ 20443 try { 20444 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 20445 const data = await res.json().catch(()=>({ok:false})); 20446 if (data.ok && typeof data.items_count === 'number') { 20447 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20448 if (el) { 20449 el.textContent = String(data.items_count); 20450 el.classList.toggle('is-empty', data.items_count <= 0); 20451 } 20452 // Dispatch event 20453 try { 20454 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 20455 } catch(_){} 20456 } 20457 } catch (err) { 20458 console.log('Could not update cart counter:', err); 20459 } 20460 } 20461 20462 // Function to manually merge guest cart (if needed) 20463 async function mergeGuestCart(){ 20464 try { 20465 const res = await fetch('/api/cart/merge-guest.php', { 20466 method: 'POST', 20467 headers: { 20468 'Content-Type': 'application/json', 20469 'X-CSRF-Token': getCsrfToken() 20470 }, 20471 credentials: 'same-origin', 20472 body: JSON.stringify({ csrf_token: getCsrfToken() }) 20473 }); 20474 const data = await res.json().catch(()=>({ok:false})); 20475 20476 if (data.ok && data.merged_items > 0) { 20477 console.log(`Guest cart merged: ${data.merged_items} items`); 20478 // Update cart counter after merge 20479 await updateCartCounter(); 20480 return data; 20481 } 20482 return data; 20483 } catch (err) { 20484 console.log('Could not merge guest cart:', err); 20485 return { ok: false, error: err.message }; 20486 } 20487 } 20488 20489 window.addToCart = addToCart; 20490 window.updateCartCounter = updateCartCounter; 20491 window.mergeGuestCart = mergeGuestCart; 20492 20493 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 20494 document.addEventListener('click', async (e)=>{ 20495 const btn = e.target.closest('[data-addtocart]'); 20496 if (!btn) return; 20497 e.preventDefault(); 20498 if (btn.disabled) return; 20499 20500 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 20501 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
20502 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 20503 20504 try { 20505 const data = await addToCart(pid, qty, vid); 20506 if (window.showToast) { 20507 const meta = guessPdMeta(); 20508 window.showToast({ 20509 type:'success', 20510 title:'Berhasil ditambahkan', 20511 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 20512 duration: 3800, 20513 actions: [ 20514 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 20515 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 20516 ] 20517 }); 20518 } 20519 } catch (err) { 20520 if (window.showToast) { 20521 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 20522 } else { 20523 console.error(err); 20524 } 20525 } 20526 }); 20527})(); 20528</script>
vendor: 2 bytes, line 20528
20528
20529<script data-inlined="mega-menu-tiles.js"> 20530// Tile Mega Menu â Enhanced 2025 (Stagger animations, product counts, loading states) 20531// Includes safe-area hover handling (no backdrop flicker) 20532 20533(function(){ 20534 const trigger = document.querySelector('[data-mm2="trigger"]'); 20535 const panel = document.querySelector('[data-mm2="panel"]'); 20536 const topView = document.querySelector('[data-mm2="top"]'); 20537 const detailView= document.querySelector('[data-mm2="detail"]'); 20538 const backdrop = document.querySelector('[data-mm2="backdrop"]'); 20539 if (!trigger || !panel || !topView || !detailView) return; 20540 20541 const apiUrl = '/api/categories_tree.php?top=16&child=18&grand=14'; 20542 const isDesktop = () => window.matchMedia('(min-width: 961px)').matches; 20543 20544 let data = []; 20545 let hoverTimer, closeTimer; 20546 let isLoading = false; 20547 20548 const esc = s => (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 20549 20550 // Mobile close button HTML 20551 const mobileCloseBtn = () => ` 20552 <button type="button" class="mm2-mobile-close" data-mm2="close" aria-label="Tutup menu"> 20553 <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"> 20554 <line x1="18" y1="6" x2="6" y2="18"></line> 20555 <line x1="6" y1="6" x2="18" y2="18"></line> 20556 </svg> 20557 </button> 20558 `; 20559 20560 // Category-specific gradient colors (16 unique combinations) 20561 const categoryGradients = [ 20562 { from: '#e0e7ff', to: '#fff', border: '#c7d2fe', color: '#4f46e5' }, // Indigo 20563 { from: '#dbeafe', to: '#fff', border: '#bfdbfe', color: '#1e40af' }, // Blue 20564 { from: '#d1fae5', to: '#fff', border: '#a7f3d0', color: '#059669' }, // Green 20565 { from: '#fef3c7', to: '#fff', border: '#fde68a', color: '#d97706' }, // Amber 20566 { from: '#fce7f3', to: '#fff', border: '#fbcfe8', color: '#db2777' }, // Pink 20567 { from: '#e9d5ff', to: '#fff', border: '#d8b4fe', color: '#9333ea' }, // Purple 20568 { from: '#ffedd5', to: '#fff', border: '#fed7aa', color: '#ea580c' }, // Orange 20569 { from: '#ccfbf1', to: '#fff', border: '#99f6e4', color: '#0d9488' }, // Teal 20570 { from: '#fef9c3', to: '#fff', border: '#fef08a', color: '#ca8a04' }, // Yellow 20571 { from: '#ffe4e6', to: '#fff', border: '#fecdd3', color: '#e11d48' }, // Rose 20572 { from: '#e0f2fe', to: '#fff', border: '#bae6fd', color: '#0284c7' }, // Sky 20573 { from: '#f3e8ff', to: '#fff', border: '#e9d5ff', color: '#a855f7' }, // Violet 20574 { from: '#dcfce7', to: '#fff', border: '#bbf7d0', color: '#16a34a' }, // Emerald 20575 { from: '#fef2f2', to: '#fff', border: '#fecaca', color: '#dc2626' }, // Red 20576 { from: '#eff6ff', to: '#fff', border: '#dbeafe', color: '#2563eb' }, // Blue-600 20577 { from: '#f5f3ff', to: '#fff', border: '#ede9fe', color: '#7c3aed' }, // Violet-600 20578 ]; 20579 20580 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>`; 20581 20582 // Recent (optional future use) 20583 const REC_KEY='mm2_recent'; 20584 const saveRecent=(path,label)=>{ try{ 20585 let list=JSON.parse(localStorage.getItem(REC_KEY)||'[]').filter(x=>x&&x.path!==path); 20586 list.unshift({path,label,t:Date.now()}); localStorage.setItem(REC_KEY, JSON.stringify(list.slice(0,8))); 20587 }catch{} }; 20588 20589 function isToSafeArea(e){
20590 const t = e && e.relatedTarget ? e.relatedTarget : null; 20591 if (!t) return false; 20592 return (trigger.contains(t) || panel.contains(t) || (backdrop && backdrop.contains(t))); 20593 } 20594 20595 function open(){ 20596 document.body.classList.add('mm2-open'); 20597 showTop(); 20598 // Add entrance animation class 20599 setTimeout(() => panel.classList.add('mm2-animated'), 10); 20600 } 20601 function close(){ 20602 document.body.classList.remove('mm2-open'); 20603 panel.classList.remove('mm2-animated'); 20604 } 20605 function showTop(){ 20606 topView.classList.add('is-active'); 20607 detailView.classList.remove('is-active'); 20608 } 20609 20610 // Loading skeleton 20611 function showLoading() { 20612 const skeletons = Array.from({length: 12}, (_, i) => ` 20613 <div class="mm2-card mm2-skeleton" style="animation-delay: ${i * 0.02}s"> 20614 <span class="mm2-ico skeleton-box"></span> 20615 <span style="flex: 1;"> 20616 <div class="skeleton-line" style="width: 60%; height: 16px; margin-bottom: 6px;"></div> 20617 <div class="skeleton-line" style="width: 40%; height: 12px;"></div> 20618 </span> 20619 </div> 20620 `).join(''); 20621 topView.innerHTML = `<div class="mm2-top"><div class="mm2-grid">${skeletons}</div></div>`; 20622 } 20623 20624 // Count total products in a category tree 20625 function countProducts(node) { 20626 let count = node.product_count || 0; 20627 if (Array.isArray(node.children)) { 20628 node.children.forEach(child => { 20629 count += countProducts(child); 20630 }); 20631 } 20632 return count; 20633 } 20634 20635 // Cache for fetched thumbnails to avoid duplicate requests 20636 const thumbnailCache = new Map(); 20637 20638 // Fetch product thumbnails for a category 20639 function fetchCategoryThumbnails(categoryPath) { 20640 // Check cache first 20641 if (thumbnailCache.has(categoryPath)) { 20642 injectThumbnails(categoryPath, thumbnailCache.get(categoryPath)); 20643 return; 20644 } 20645 20646 const url = `/api/category_products_preview.php?path=${encodeURIComponent(categoryPath)}&limit=2`; 20647 20648 fetch(url) 20649 .then(r => { 20650 if (!r.ok) throw new Error('Network error'); 20651 return r.json(); 20652 }) 20653 .then(data => { 20654 if (data.success && data.products && data.products.length > 0) { 20655 thumbnailCache.set(categoryPath, data.products); 20656 injectThumbnails(categoryPath, data.products); 20657 } else { 20658 // No products, remove skeleton 20659 removeThumbSkeletons(categoryPath); 20660 } 20661 }) 20662 .catch(err => { 20663 console.warn('Failed to load thumbnails for', categoryPath, err); 20664 removeThumbSkeletons(categoryPath); 20665 }); 20666 } 20667 20668 // Inject thumbnails into the DOM 20669 function injectThumbnails(categoryPath, products) { 20670 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20671 links.forEach(link => { 20672 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20673 if (!thumbsContainer) return; 20674 20675 const thumbsHtml = products.slice(0, 2).map(p => 20676 `<img src="${esc(p.img)}" alt="${esc(p.name)}" class="mm2-thumb" title="${esc(p.name)}" loading="lazy">` 20677 ).join(''); 20678 20679 thumbsContainer.innerHTML = thumbsHtml; 20680 thumbsContainer.removeAttribute('data-loading'); 20681 }); 20682 } 20683 20684 // Remove skeleton loaders when no products 20685 function removeThumbSkeletons(categoryPath) { 20686 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20687 links.forEach(link => { 20688 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20689 if (thumbsContainer) { 20690 thumbsContainer.remove(); 20691 } 20692 }); 20693 } 20694 20695 function showDetail(node){ 20696 const title = node.label.split('/')[0]; 20697 const children = Array.isArray(node.children) ? node.children : []; 20698 const hasChildren = children.length > 0; 20699 20700 const subnav = hasChildren ? ` 20701 <nav class="mm2-subnav" role="tablist"> 20702 ${children.map((c2,i)=>{ 20703 const t = c2.label.split('/').slice(-1)[0]; 20704 return `<button class="mm2-subitem${i===0?' active':''}" role="tab" data-idx="${i}" aria-selected="${i===0?'true':'false'}">${esc(t)}</button>`; 20705 }).join('')} 20706 </nav>` : ''; 20707 20708 const subcontent = `<div class="mm2-subcontent" data-mm2="subcontent"></div>`; 20709
20710 detailView.innerHTML = ` 20711 <div class="mm2-detail"> 20712 <div class="mm2-detail-header"> 20713 <button class="mm2-back" type="button" data-mm2="back" aria-label="Kembali"> 20714 <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> 20715 </button> 20716 <div class="mm2-detail-title">${esc(title)}</div> 20717 </div> 20718 20719 ${hasChildren ? ` 20720 <div class="mm2-detail-layout"> 20721 ${subnav} 20722 ${subcontent} 20723 </div>` : ` 20724 <div class="mm2-links"> 20725 <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)}"> 20726 <strong>Lihat semua ${esc(title)}</strong> 20727 </a> 20728 </div> 20729 `} 20730 </div> 20731 `; 20732 20733 topView.classList.remove('is-active'); 20734 detailView.classList.add('is-active'); 20735 20736 if (hasChildren) { 20737 const subEl = detailView.querySelector('.mm2-subnav'); 20738 const contEl = detailView.querySelector('[data-mm2="subcontent"]'); 20739 let activeIdx = 0; 20740 20741 const renderSubgrid = (idx) => { 20742 const c2 = children[idx]; if (!c2) { contEl.innerHTML=''; return; } 20743 const title2 = c2.label.split('/').slice(-1)[0]; 20744 const l3 = Array.isArray(c2.children) ? c2.children : []; 20745 const l3Html = l3.map(c3 => ` 20746 <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)}"> 20747 <span class="mm2-link-text">${esc(c3.label.split('/').slice(-1)[0])}</span> 20748 <span class="mm2-thumbs" data-loading="true"> 20749 <span class="mm2-thumb-skeleton"></span> 20750 <span class="mm2-thumb-skeleton"></span> 20751 </span> 20752 </a>`).join(''); 20753 20754 contEl.innerHTML = ` 20755 <div class="mm2-links"> 20756 <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)}"> 20757 <strong>Lihat semua ${esc(title2)}</strong> 20758 </a> 20759 ${l3Html} 20760 </div> 20761 `; 20762 20763 // Fetch product thumbnails for each level-3 category (desktop only) 20764 if (isDesktop()) { 20765 setTimeout(() => { 20766 l3.forEach(c3 => { 20767 fetchCategoryThumbnails(c3.path); 20768 }); 20769 }, 100); 20770 } 20771 }; 20772 20773 renderSubgrid(activeIdx); 20774 20775 // Desktop: hover ganti konten; Mobile & Desktop: click pilih 20776 subEl.addEventListener('mousemove', (e)=>{ 20777 if (!isDesktop()) return; 20778 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20779 const idx = +it.dataset.idx; if (!Number.isInteger(idx) || idx===activeIdx) return; 20780 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20781 it.classList.add('active'); 20782 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20783 activeIdx = idx; 20784 renderSubgrid(activeIdx); 20785 }); 20786 20787 subEl.addEventListener('click', (e)=>{ 20788 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20789 const idx = +it.dataset.idx; if (!Number.isInteger(idx)) return; 20790 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20791 it.classList.add('active'); 20792 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20793 activeIdx = idx; 20794 renderSubgrid(activeIdx); 20795 }); 20796 20797 // Delegate link clicks to add recent 20798 detailView.addEventListener('click', (e)=>{ 20799 const nav = e.target.closest('[data-mm2="nav"]'); 20800 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20801 }); 20802 } 20803 } 20804 20805 function buildTop(){ 20806 const html = data.map((n, idx) => { 20807 const title = n.label.split('/')[0]; 20808 const childCount = (n.children||[]).length;
20809 const productCount = countProducts(n); 20810 const gradient = categoryGradients[idx % categoryGradients.length]; 20811 20812 // Dynamic subtitle based on content - shorter for mobile 20813 let sub = ''; 20814 if (productCount > 0) { 20815 sub = `${productCount} produk`; 20816 if (childCount > 0) sub += ` · ${childCount} kat`; 20817 } else if (childCount > 0) { 20818 sub = `${childCount} kategori`; 20819 } else { 20820 sub = 'Lihat semua'; 20821 } 20822 20823 const iconStyle = `background: linear-gradient(135deg, ${gradient.from}, ${gradient.to}); border-color: ${gradient.border}; color: ${gradient.color};`; 20824 20825 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)}"> 20826 <span class="mm2-ico" style="${iconStyle}">${icon()}</span> 20827 <span style="flex: 1;"> 20828 <div class="mm2-title">${esc(title)}</div> 20829 <div class="mm2-sub">${esc(sub)}</div> 20830 </span> 20831 </a>`; 20832 }).join(''); 20833 const closeBtnHtml = !isDesktop() ? mobileCloseBtn() : ''; 20834 topView.innerHTML = `<div class="mm2-top">${closeBtnHtml}<div class="mm2-grid">${html}</div></div>`; 20835 } 20836 20837 // Open/close with safe-area 20838 trigger.addEventListener('click', (e)=>{ e.preventDefault(); document.body.classList.contains('mm2-open') ? close() : open(); }); 20839 trigger.addEventListener('pointerenter', ()=>{ if (isDesktop()) { clearTimeout(closeTimer); hoverTimer=setTimeout(open, 80); }}); 20840 trigger.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) { clearTimeout(hoverTimer); closeTimer=setTimeout(close, 200); }}); 20841 panel.addEventListener('pointerenter', ()=>{ if (isDesktop()) clearTimeout(closeTimer); }); 20842 panel.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) closeTimer=setTimeout(close, 200); }); 20843 backdrop?.addEventListener('pointerenter', ()=>{ clearTimeout(closeTimer); }); 20844 backdrop?.addEventListener('click', close); 20845 document.addEventListener('keydown', (e)=>{ if (e.key==='Escape') close(); }); 20846 20847 // Card click â open detail (unless cmd/ctrl to open link) 20848 panel.addEventListener('click', (e)=>{ 20849 const closeBtn = e.target.closest('[data-mm2="close"]'); if (closeBtn) { e.preventDefault(); close(); return; } 20850 const back = e.target.closest('[data-mm2="back"]'); if (back) { e.preventDefault(); showTop(); return; } 20851 const card = e.target.closest('[data-mm2="card"]'); 20852 if (card && !e.metaKey && !e.ctrlKey) { 20853 e.preventDefault(); 20854 const n = data.find(d => d.path === card.dataset.path); 20855 if (n) showDetail(n); 20856 } 20857 const nav = e.target.closest('[data-mm2="nav"]'); 20858 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20859 }); 20860 20861 // Swipe-down gesture for mobile (bottom sheet close) 20862 if (!isDesktop()) { 20863 let startY = 0, currentY = 0, isDragging = false, startTime = 0; 20864 const SWIPE_THRESHOLD = 80; 20865 const VELOCITY_THRESHOLD = 0.3; 20866 20867 panel.addEventListener('touchstart', (e) => { 20868 if (!document.body.classList.contains('mm2-open')) return; 20869 const touch = e.touches[0]; 20870 const scrollableContent = e.target.closest('.mm2-top, .mm2-detail'); 20871 20872 // Prevent drag if content is scrolled 20873 if (scrollableContent && scrollableContent.scrollTop > 5) return; 20874 20875 // Only start drag from top area or handle 20876 const rect = panel.getBoundingClientRect(); 20877 if (touch.clientY - rect.top > 60) return; 20878 20879 startY = touch.clientY; 20880 currentY = startY; 20881 startTime = Date.now(); 20882 isDragging = true; 20883 }, { passive: true }); 20884 20885 panel.addEventListener('touchmove', (e) => { 20886 if (!isDragging) return; 20887 const touch = e.touches[0]; 20888 currentY = touch.clientY; 20889 const deltaY = currentY - startY; 20890 20891 // Only allow downward swipe 20892 if (deltaY > 0) { 20893 const opacity = Math.max(0.4, 1 - (deltaY / 300)); 20894 panel.style.transform = `translateX(0) translateY(${deltaY}px)`; 20895 panel.style.opacity = opacity; 20896 } 20897 }, { passive: true });
20898 20899 const endDrag = () => { 20900 if (!isDragging) return; 20901 const deltaY = currentY - startY; 20902 const deltaTime = Date.now() - startTime; 20903 const velocity = deltaY / deltaTime; 20904 20905 // Close if swiped down enough or fast swipe 20906 if (deltaY > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) { 20907 close(); 20908 } 20909 20910 // Reset position 20911 panel.style.transform = ''; 20912 panel.style.opacity = ''; 20913 isDragging = false; 20914 }; 20915 20916 panel.addEventListener('touchend', endDrag, { passive: true }); 20917 panel.addEventListener('touchcancel', endDrag, { passive: true }); 20918 } 20919 20920 // Fetch data with loading state 20921 if (!isLoading) { 20922 isLoading = true; 20923 showLoading(); 20924 20925 fetch(apiUrl, { headers: { 'Accept': 'application/json' }}) 20926 .then(r => { 20927 if (!r.ok) throw new Error('Network response was not ok'); 20928 return r.json(); 20929 }) 20930 .then(j => { 20931 data = Array.isArray(j.data) ? j.data : []; 20932 if (!data.length) { 20933 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20934 <p>Tidak ada kategori tersedia</p> 20935 </div></div>`; 20936 return; 20937 } 20938 buildTop(); 20939 isLoading = false; 20940 }) 20941 .catch(err => { 20942 console.error('Failed to load categories:', err); 20943 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20944 <p>Gagal memuat kategori. Silakan coba lagi.</p> 20945 </div></div>`; 20946 isLoading = false; 20947 }); 20948 } 20949})(); 20950 20951// Add skeleton animation styles 20952const style = document.createElement('style'); 20953style.textContent = ` 20954.mm2-skeleton { 20955 pointer-events: none; 20956 animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; 20957} 20958.skeleton-box, .skeleton-line { 20959 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 20960 background-size: 200% 100%; 20961 animation: shimmer 1.5s infinite; 20962 border-radius: 8px; 20963} 20964@keyframes pulse { 20965 0%, 100% { opacity: 1; } 20966 50% { opacity: 0.6; } 20967} 20968@keyframes shimmer { 20969 0% { background-position: 200% 0; } 20970 100% { background-position: -200% 0; } 20971} 20972`; 20973document.head.appendChild(style); 20974</script>
20974 20975
20975<script data-inlined="benchmark.js"> 20976/** 20977 * Benchmark Data Loader 20978 * Handles AJAX fetch and display of benchmark data 20979 */ 20980 20981(function() { 20982 'use strict'; 20983 20984 // Wait for DOM ready 20985 if (document.readyState === 'loading') { 20986 document.addEventListener('DOMContentLoaded', initBenchmark); 20987 } else { 20988 initBenchmark(); 20989 } 20990 20991 function initBenchmark() { 20992 const section = document.querySelector('.pd-benchmark-section'); 20993 if (!section) return; 20994 20995 const button = document.getElementById('btn-load-benchmark'); 20996 const content = document.getElementById('benchmark-content'); 20997 const productId = section.dataset.productId; 20998 20999 if (!button || !content || !productId) return; 21000 21001 let isLoading = false; 21002 let dataLoaded = false; 21003 21004 button.addEventListener('click', function() { 21005 if (isLoading || dataLoaded) return; 21006 21007 loadBenchmarkData(productId, button, content); 21008 }); 21009 } 21010 21011 function loadBenchmarkData(productId, button, container) { 21012 // Update button state 21013 button.disabled = true; 21014 button.classList.add('loading'); 21015 const originalText = button.querySelector('.btn-text').textContent; 21016 button.querySelector('.btn-text').textContent = 'Memuat data...'; 21017 21018 // Show loading state 21019 container.style.display = 'block'; 21020 container.innerHTML = ` 21021 <div class="benchmark-loading"> 21022 <div class="benchmark-loading-spinner"></div> 21023 <p>Mengambil data benchmark...</p> 21024 </div> 21025 `; 21026 21027 // Fetch data 21028 fetch('/api/benchmark/fetch.php?product_id=' + productId) 21029 .then(function(response) { 21030 if (!response.ok) { 21031 throw new Error('HTTP error ' + response.status); 21032 } 21033 return response.json(); 21034 }) 21035 .then(function(result) { 21036 if (result.success && result.data) { 21037 renderBenchmarkData(result.data, container); 21038 button.style.display = 'none'; // Hide button after successful load 21039 } else { 21040 throw new Error(result.error || 'Data tidak tersedia'); 21041 } 21042 }) 21043 .catch(function(error) { 21044 console.error('Benchmark fetch error:', error); 21045 container.innerHTML = ` 21046 <div class="benchmark-error"> 21047 <p><strong>â ï¸ Data Benchmark Tidak Tersedia</strong></p> 21048 <p style="font-size: 14px; margin-top: 8px;"> 21049 ${error.message || 'Terjadi kesalahan saat memuat data'} 21050 </p> 21051 <button type="button" onclick="location.reload()" 21052 style="margin-top: 16px; padding: 8px 20px; background: #667eea; color: #fff; border: none; border-radius: 6px; cursor: pointer;"> 21053 Coba Lagi 21054 </button> 21055 </div> 21056 `; 21057 21058 // Reset button 21059 button.disabled = false; 21060 button.classList.remove('loading'); 21061 button.querySelector('.btn-text').textContent = originalText; 21062 }); 21063 } 21064 21065 function renderBenchmarkData(data, container) { 21066 let html = '<div class="benchmark-data-wrapper">'; 21067 21068 // Chipset info if available 21069 if (data.chipset) { 21070 html += ` 21071 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px 16px; border-radius: 8px; margin-bottom: 12px;"> 21072 <div style="font-size: 11px; opacity: 0.85; margin-bottom: 2px;">Chipset</div> 21073 <div style="font-size: 15px; font-weight: 600;">${escapeHtml(data.chipset)}</div> 21074 </div> 21075 `; 21076 } 21077 21078 // Main benchmark scores 21079 if (data.antutu || data.geekbench_multi || data.overall_score) { 21080 html += '<div class="benchmark-data-grid">'; 21081 21082 if (data.antutu) { 21083 html += ` 21084 <div class="benchmark-card"> 21085 <div class="benchmark-card-label">ANTUTU</div> 21086 <div class="benchmark-card-value large">${formatNumber(data.antutu)}</div> 21087 ${getScoreBadge(data.antutu, 'antutu')} 21088 </div> 21089 `; 21090 } 21091 21092 if (data.geekbench_multi) { 21093 html += ` 21094 <div class="benchmark-card"> 21095 <div class="benchmark-card-label">GEEKBENCH MULTI</div> 21096 <div class="benchmark-card-value">${formatNumber(data.geekbench_multi)}</div> 21097 ${data.geekbench_single ? '<div class="benchmark-card-subtitle">Single: ' + formatNumber(data.geekbench_single) + '</div>' : ''} 21098 </div> 21099 `; 21100 } 21101 21102 if (data.gaming_score) { 21103 html += ` 21104 <div class="benchmark-card"> 21105 <div class="benchmark-card-label">GAMING</div> 21106 <div class="benchmark-card-value">${data.gaming_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21107 </div> 21108 `; 21109 } 21110 21111 if (data.battery_score) { 21112 html += ` 21113 <div class="benchmark-card"> 21114 <div class="benchmark-card-label">BATTERY</div> 21115 <div class="benchmark-card-value">${data.battery_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21116 </div> 21117 `; 21118 } 21119 21120 if (data.overall_score) { 21121 html += ` 21122 <div class="benchmark-card"> 21123 <div class="benchmark-card-label">OVERALL</div> 21124 <div class="benchmark-card-value">${data.overall_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21125 </div> 21126 `; 21127 } 21128 21129 html += '</div>'; 21130 } 21131 21132 // AI Performance Description 21133 if (data.notes) { 21134 html += ` 21135 <div style="background: #f8f9fa; border-left: 3px solid #667eea; padding: 10px 14px; border-radius: 6px; margin-top: 12px;"> 21136 <div style="font-size: 11px; font-weight: 600; color: #667eea; margin-bottom: 6px;">ð¡ Analisis Performa</div> 21137 <div style="font-size: 13px; color: #4a5568; line-height: 1.5;">${escapeHtml(data.notes)}</div> 21138 </div> 21139 `; 21140 } 21141 21142 // Attribution 21143 html += ` 21144 <div class="benchmark-attribution"> 21145 <p style="font-size: 11px; margin: 8px 0 0 0; color: #888;">Data benchmark dianalisis menggunakan AI dari spesifikasi produk</p> 21146 ${data.fetched_at ? '<p style="font-size: 10px; margin: 4px 0 0 0; color: #aaa;">Terakhir diperbarui: ' + formatDate(data.fetched_at) + '</p>' : ''} 21147 </div> 21148 `; 21149 21150 html += '</div>'; 21151 21152 container.innerHTML = html; 21153 } 21154 21155 function formatNumber(num) { 21156 if (!num) return '-'; 21157 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 21158 } 21159 21160 function escapeHtml(text) { 21161 if (!text) return ''; 21162 const div = document.createElement('div'); 21163 div.textContent = text; 21164 return div.innerHTML; 21165 } 21166 21167 function formatDate(dateStr) { 21168 try { 21169 const date = new Date(dateStr); 21170 const options = { year: 'numeric', month: 'long', day: 'numeric' }; 21171 return date.toLocaleDateString('id-ID', options); 21172 } catch (e) { 21173 return dateStr; 21174 } 21175 } 21176 21177 function getScoreBadge(score, type) { 21178 let category = ''; 21179 let label = ''; 21180 21181 if (type === 'antutu') { 21182 if (score >= 500000) { 21183 category = 'excellent'; 21184 label = 'Flagship'; 21185 } else if (score >= 350000) { 21186 category = 'good'; 21187 label = 'High-End'; 21188 } else if (score >= 200000) { 21189 category = 'average'; 21190 label = 'Mid-Range'; 21191 } else { 21192 category = 'average'; 21193 label = 'Entry-Level'; 21194 } 21195 } 21196 21197 if (label) { 21198 return '<div class="benchmark-score-badge ' + category + '">' + label + '</div>'; 21199 } 21200 21201 return ''; 21202 } 21203})(); 21204 21205</script>
21205 21206
21206<script data-inlined="product-compare.js"> 21207(function() { 21208 'use strict'; 21209 21210 // State management 21211 let compareList = []; 21212 let isAnalyzing = false; 21213 let currentAnalysis = null; 21214 21215 // DOM elements 21216 const compareBtn = document.getElementById('compareBtn'); 21217 const compareSidebar = document.getElementById('compareSidebar'); 21218 const compareOverlay = document.getElementById('compareOverlay'); 21219 const compareClose = document.getElementById('compareClose'); 21220 const compareModal = document.getElementById('aiCompareModal'); 21221 const compareModalClose = document.getElementById('aiCompareModalClose'); 21222 21223 // Initialize 21224 document.addEventListener('DOMContentLoaded', async function() { 21225 await initializeCompare(); 21226 setupEventListeners(); 21227 }); 21228 21229 /** 21230 * Initialize compare functionality 21231 */ 21232 async function initializeCompare() { 21233 // Load compare list first before setting up buttons 21234 await loadCompareList(); 21235 21236 // Setup product detail compare button if exists 21237 const productCompareBtn = document.getElementById('pd-compare-btn'); 21238 21239 if (productCompareBtn) { 21240 setupProductCompareButton(productCompareBtn); 21241 } 21242 21243 // Check for existing compare list and update UI 21244 if (compareList.length > 0) { 21245 // Only update badge and buttons, no sticky bar 21246 } 21247 } 21248 21249 /** 21250 * Setup event listeners 21251 */ 21252 function setupEventListeners() { 21253 // Navigation compare button 21254 if (compareBtn) { 21255 compareBtn.addEventListener('click', toggleCompareSidebar); 21256 } 21257 21258 // Sidebar controls 21259 if (compareClose) { 21260 compareClose.addEventListener('click', closeCompareSidebar); 21261 } 21262 21263 if (compareOverlay) { 21264 compareOverlay.addEventListener('click', closeCompareSidebar); 21265 } 21266 21267 // Sidebar actions 21268 const clearAllBtn = document.getElementById('compareClearAll'); 21269 if (clearAllBtn) { 21270 clearAllBtn.addEventListener('click', clearAllProducts); 21271 } 21272 21273 const analyzeBtn = document.getElementById('compareAnalyze'); 21274 if (analyzeBtn) { 21275 analyzeBtn.addEventListener('click', startAIAnalysis); 21276 } 21277 21278 // Modal controls 21279 if (compareModalClose) { 21280 compareModalClose.addEventListener('click', closeModal); 21281 } 21282 21283 const modalBackdrop = compareModal?.querySelector('.modal-backdrop'); 21284 if (modalBackdrop) { 21285 modalBackdrop.addEventListener('click', closeModal); 21286 } 21287 21288 // Modal actions 21289 const retryBtn = document.getElementById('aiRetry'); 21290 if (retryBtn) { 21291 retryBtn.addEventListener('click', startAIAnalysis); 21292 } 21293 21294 // Removed share and add to cart buttons - replaced with AI disclaimer 21295 } 21296 21297 /** 21298 * Setup product detail compare button 21299 */ 21300 function setupProductCompareButton(button) { 21301 const productId = parseInt(button.dataset.productId); 21302 if (!productId) return; 21303 21304 // Check if product is already in compare list 21305 updateProductCompareButtonState(button, productId); 21306 21307 button.addEventListener('click', async function() { 21308 // Get current variant ID (if any) 21309 const variantIdElement = document.getElementById('pd-variant-id'); 21310 const variantId = variantIdElement ? parseInt(variantIdElement.value) || 0 : 0; 21311 21312 // Update button's variant data for current selection 21313 button.dataset.variantId = variantId.toString(); 21314 21315 await toggleProductInCompare(productId, button, variantId); 21316 }); 21317 } 21318 21319 /** 21320 * Toggle product in compare list 21321 */ 21322 async function toggleProductInCompare(productId, button, variantId = 0) { 21323 // Check by product ID (variants of same product treated as same item) 21324 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21325 21326 try { 21327 if (isInCompare) { 21328 await removeFromCompare(productId); 21329 showToast('Produk dihapus dari perbandingan', 'success'); 21330 } else { 21331 await addToCompare(productId, variantId); 21332 showToast('Produk ditambahkan ke perbandingan', 'success'); 21333 21334 // Trigger fly-to-compare animation 21335 triggerFlyToCompareAnimation(button); 21336 } 21337 21338 updateProductCompareButtonState(button, productId); 21339 updateCompareBadge(); 21340 updateAllCompareButtons(); 21341 21342 } catch (error) { 21343 showToast(error.message || 'Terjadi kesalahan', 'error'); 21344 } 21345 } 21346 21347 /** 21348 * Update product compare button state 21349 */ 21350 function updateProductCompareButtonState(button, productId) { 21351 // Ensure both IDs are compared as integers 21352 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21353 button.classList.toggle('added', isInCompare); 21354 21355 // Handle visibility of text elements 21356 const compareText = button.querySelector('.compare-text'); 21357 const compareRemove = button.querySelector('.compare-remove'); 21358 const compareNote = document.getElementById('compare-note'); 21359 21360 if (compareText) { 21361 compareText.style.display = isInCompare ? 'none' : 'inline'; 21362 } 21363 21364 if (compareRemove) { 21365 if (isInCompare) { 21366 compareRemove.removeAttribute('hidden'); 21367 compareRemove.style.display = 'flex'; 21368 } else { 21369 compareRemove.setAttribute('hidden', ''); 21370 compareRemove.style.display = 'none'; 21371 } 21372 } 21373 21374 // Update info note 21375 if (compareNote) { 21376 if (isInCompare) { 21377 const currentCount = compareList.length; 21378 compareNote.textContent = `Produk sudah ada di daftar perbandingan (${currentCount}/5). Klik tombol untuk menghapus.`; 21379 } else { 21380 compareNote.textContent = 'Bandingkan hingga 5 produk dengan bantuan AI'; 21381 } 21382 } 21383 21384 // Inline compare preview removed - keeping it simple 21385 } 21386 21387 /** 21388 * Add product to compare list 21389 */ 21390 async function addToCompare(productId, variantId = 0) { 21391 const requestData = { product_id: productId }; 21392 if (variantId > 0) { 21393 requestData.variant_id = variantId; 21394 } 21395 21396 const response = await fetch('/api/compare/add.php', { 21397 method: 'POST', 21398 headers: { 21399 'Content-Type': 'application/json', 21400 }, 21401 body: JSON.stringify(requestData), 21402 credentials: 'same-origin' 21403 });
21404 21405 const result = await response.json(); 21406 21407 if (!result.success) { 21408 throw new Error(result.error || 'Gagal menambahkan produk'); 21409 } 21410 21411 // Send Google Analytics event for add to compare 21412 if (typeof gtag === 'function') { 21413 gtag('event', 'add_to_compare', { 21414 event_category: 'Product Compare', 21415 event_label: 'Product Added to Compare', 21416 product_id: productId, 21417 variant_id: variantId || 0, 21418 custom_map: { 21419 custom_parameter_1: 'compare_add_product' 21420 } 21421 }); 21422 } 21423 21424 await loadCompareList(); 21425 return result; 21426 } 21427 21428 /** 21429 * Remove product from compare list 21430 */ 21431 async function removeFromCompare(productId) { 21432 const response = await fetch('/api/compare/remove.php', { 21433 method: 'POST', 21434 headers: { 21435 'Content-Type': 'application/json', 21436 }, 21437 body: JSON.stringify({ product_id: productId }), 21438 credentials: 'same-origin' 21439 }); 21440 21441 const result = await response.json(); 21442 21443 if (!result.success) { 21444 throw new Error(result.error || 'Gagal menghapus produk'); 21445 } 21446 21447 // Send Google Analytics event for remove from compare 21448 if (typeof gtag === 'function') { 21449 gtag('event', 'remove_from_compare', { 21450 event_category: 'Product Compare', 21451 event_label: 'Product Removed from Compare', 21452 product_id: productId, 21453 custom_map: { 21454 custom_parameter_1: 'compare_remove_product' 21455 } 21456 }); 21457 } 21458 21459 await loadCompareList(); 21460 return result; 21461 } 21462 21463 /** 21464 * Clear all products from compare list 21465 */ 21466 async function clearAllProducts() { 21467 if (compareList.length === 0) return; 21468 21469 // Show confirmation modal 21470 const confirmed = await showConfirmModal({ 21471 title: 'Hapus Semua Produk', 21472 message: 'Apakah Anda yakin ingin menghapus semua produk dari daftar perbandingan?', 21473 confirmText: 'Hapus Semua', 21474 cancelText: 'Batal', 21475 type: 'danger' 21476 }); 21477 21478 if (!confirmed) return; 21479 21480 try { 21481 const response = await fetch('/api/compare/clear.php', { 21482 method: 'POST', 21483 credentials: 'same-origin' 21484 }); 21485 21486 const result = await response.json(); 21487 21488 if (!result.success) { 21489 throw new Error(result.error || 'Gagal menghapus produk'); 21490 } 21491 21492 // Send Google Analytics event for clear all products 21493 if (typeof gtag === 'function') { 21494 gtag('event', 'compare_clear_all', { 21495 event_category: 'Product Compare', 21496 event_label: 'Clear All Products', 21497 products_count: compareList.length, 21498 custom_map: { 21499 custom_parameter_1: 'compare_clear_action' 21500 } 21501 }); 21502 } 21503 21504 await loadCompareList(); 21505 showToast('Semua produk dihapus dari perbandingan', 'success'); 21506 21507 } catch (error) { 21508 showToast(error.message || 'Terjadi kesalahan', 'error'); 21509 } 21510 } 21511 21512 /** 21513 * Load compare list from server 21514 */ 21515 async function loadCompareList() { 21516 try { 21517 const response = await fetch('/api/compare/get.php', { 21518 credentials: 'same-origin' 21519 }); 21520 21521 const result = await response.json(); 21522 21523 if (result.success) { 21524 compareList = result.products || []; 21525 updateCompareSidebar(); 21526 updateCompareBadge(); 21527 updateAllCompareButtons(); 21528 } 21529 21530 } catch (error) { 21531 // Silently handle error - compare functionality will work with empty list 21532 } 21533 } 21534 21535 /** 21536 * Update compare sidebar content 21537 */ 21538 function updateCompareSidebar() { 21539 const emptyState = document.getElementById('compareEmpty'); 21540 const listContainer = document.getElementById('compareList'); 21541 const actionsContainer = document.getElementById('compareActions'); 21542 const countElement = document.getElementById('compareCountSidebar'); 21543 21544 if (!emptyState || !listContainer || !actionsContainer || !countElement) return; 21545 21546 // Update count 21547 countElement.textContent = `(${compareList.length})`; 21548 21549 if (compareList.length === 0) { 21550 emptyState.style.display = 'block'; 21551 listContainer.style.display = 'none'; 21552 actionsContainer.style.display = 'none'; 21553 } else { 21554 emptyState.style.display = 'none'; 21555 listContainer.style.display = 'block'; 21556 actionsContainer.style.display = 'flex'; 21557 21558 // Render product list 21559 listContainer.innerHTML = compareList.map(product => { 21560 // Display variant name if available 21561 let displayName = product.name; 21562 if (product.combination_name) { 21563 displayName += ` (${product.combination_name})`; 21564 } 21565 21566 // Use variant price if available 21567 let displayPrice = product.price; 21568 if (product.variant_price !== undefined && product.variant_price !== null) { 21569 displayPrice = product.variant_price; 21570 } 21571 21572 return ` 21573 <div class="compare-item" data-product-id="${product.id}"> 21574 <div class="compare-item-image"> 21575 <img src="${product.image_url}" alt="${escapeHtml(displayName)}" loading="lazy"> 21576 </div> 21577 <div class="compare-item-info"> 21578 <div class="compare-item-name">${escapeHtml(displayName)}</div> 21579 <div class="compare-item-price">${formatPrice(displayPrice)}</div> 21580 </div> 21581 <button class="compare-item-remove" data-product-id="${product.id}" aria-label="Hapus">
21582 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 21583 <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"/> 21584 </svg> 21585 </button> 21586 </div> 21587 `; 21588 }).join(''); 21589 21590 // Setup event listeners for remove buttons 21591 setupRemoveButtons(); 21592 21593 // Update analyze button state 21594 const analyzeBtn = document.getElementById('compareAnalyze'); 21595 if (analyzeBtn) { 21596 analyzeBtn.disabled = compareList.length < 2; 21597 } 21598 } 21599 } 21600 21601 /** 21602 * Setup remove buttons event listeners 21603 */ 21604 function setupRemoveButtons() { 21605 const removeButtons = document.querySelectorAll('.compare-item-remove'); 21606 removeButtons.forEach(button => { 21607 button.addEventListener('click', async (e) => { 21608 e.preventDefault(); 21609 const productId = parseInt(button.dataset.productId); 21610 if (productId) { 21611 await removeProduct(productId); 21612 } 21613 }); 21614 }); 21615 } 21616 21617 /** 21618 * Update all compare buttons on the page 21619 */ 21620 function updateAllCompareButtons() { 21621 // Update product detail compare button 21622 const productBtn = document.getElementById('pd-compare-btn'); 21623 if (productBtn) { 21624 const productId = parseInt(productBtn.dataset.productId); 21625 if (productId) { 21626 updateProductCompareButtonState(productBtn, productId); 21627 } 21628 } 21629 21630 // Update any other compare buttons on the page 21631 const allCompareButtons = document.querySelectorAll('[data-product-id][class*="compare"]'); 21632 allCompareButtons.forEach(button => { 21633 const productId = parseInt(button.dataset.productId); 21634 if (productId && button.id !== 'pd-compare-btn') { 21635 updateProductCompareButtonState(button, productId); 21636 } 21637 }); 21638 } 21639 21640 /** 21641 * Update compare badge 21642 */ 21643 function updateCompareBadge() { 21644 const badge = document.getElementById('compareCount'); 21645 if (!badge) return; 21646 21647 const oldCount = parseInt(badge.textContent) || 0; 21648 const newCount = compareList.length; 21649 21650 badge.textContent = newCount; 21651 badge.classList.toggle('active', newCount > 0); 21652 21653 // Animate badge if count increased 21654 if (newCount > oldCount) { 21655 animateCompareBadge(); 21656 highlightCompareButton(); 21657 } 21658 21659 // Sticky bar removed - keeping only fly-to-navbar animation 21660 } 21661 21662 /** 21663 * Animate compare badge when new product added 21664 */ 21665 function animateCompareBadge() { 21666 const badge = document.getElementById('compareCount'); 21667 if (!badge) return; 21668 21669 badge.style.transform = 'scale(1.3)'; 21670 badge.style.transition = 'transform 0.3s ease'; 21671 21672 setTimeout(() => { 21673 badge.style.transform = 'scale(1)'; 21674 }, 300); 21675 } 21676 21677 /** 21678 * Highlight compare button temporarily 21679 */ 21680 function highlightCompareButton() { 21681 const compareBtn = document.getElementById('compareBtn'); 21682 if (!compareBtn) return; 21683 21684 compareBtn.classList.add('pulse-highlight'); 21685 21686 setTimeout(() => { 21687 compareBtn.classList.remove('pulse-highlight'); 21688 }, 3000); 21689 } 21690 21691 /** 21692 * Trigger fly-to-compare animation 21693 */ 21694 function triggerFlyToCompareAnimation(sourceButton) { 21695 if (!sourceButton) { 21696 return; 21697 } 21698 21699 // Check if mobile view (bottom nav) or desktop (top nav) 21700 const isMobile = window.innerWidth <= 768; 21701 21702 // Get source position 21703 const sourceRect = sourceButton.getBoundingClientRect(); 21704 21705 // Calculate target position 21706 let targetTop, targetLeft; 21707 21708 if (isMobile) { 21709 // Mobile: fly to fixed bottom position (bottom nav area) 21710 // Use fixed coordinates regardless of bottom nav visibility 21711 targetTop = window.innerHeight - 80; // Just above bottom nav area 21712 targetLeft = window.innerWidth / 2 - 30; // Center horizontally 21713 } else { 21714 // Desktop: fly to top navbar compare button 21715 const targetElement = document.getElementById('compareBtn'); 21716 if (!targetElement) { 21717 return; 21718 } 21719 const targetRect = targetElement.getBoundingClientRect(); 21720 targetTop = targetRect.top + 10; 21721 targetLeft = targetRect.left + 10; 21722 } 21723 21724 // Get product image for animation - try multiple selectors 21725 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"]'); 21726 21727 // Create animated element 21728 const flyingElement = document.createElement('div'); 21729 flyingElement.className = 'flying-product'; 21730 21731 if (productImage) { 21732 flyingElement.innerHTML = `<img src="${productImage.src}" alt="Flying product">`; 21733 flyingElement.querySelector('img').style.cssText = ` 21734 width: 100%; 21735 height: 100%; 21736 object-fit: cover; 21737 border-radius: 8px; 21738 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21739 `; 21740 } else { 21741 // Fallback: colored div with icon 21742 flyingElement.innerHTML = `<div class="flying-fallback">ð¦</div>`; 21743 flyingElement.querySelector('.flying-fallback').style.cssText = ` 21744 width: 100%; 21745 height: 100%; 21746 background: #3b82f6; 21747 border-radius: 8px; 21748 display: flex; 21749 align-items: center; 21750 justify-content: center; 21751 font-size: 24px; 21752 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21753 `; 21754 } 21755 21756 flyingElement.style.cssText = ` 21757 position: fixed; 21758 top: ${sourceRect.top}px; 21759 left: ${sourceRect.left}px; 21760 width: 60px; 21761 height: 60px; 21762 z-index: 9999; 21763 pointer-events: none; 21764 opacity: 1;
21765 transform: scale(1); 21766 `; 21767 21768 document.body.appendChild(flyingElement); 21769 21770 // Use Web Animations API for more reliable animation 21771 flyingElement.animate([ 21772 // Keyframe 1: Initial state 21773 { 21774 top: `${sourceRect.top}px`, 21775 left: `${sourceRect.left}px`, 21776 transform: 'scale(1)', 21777 opacity: 1 21778 }, 21779 // Keyframe 2: Final state 21780 { 21781 top: `${targetTop}px`, 21782 left: `${targetLeft}px`, 21783 transform: 'scale(0.3)', 21784 opacity: 0 21785 } 21786 ], { 21787 duration: 800, 21788 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 21789 fill: 'forwards' 21790 }); 21791 21792 // Clean up after animation completes 21793 setTimeout(() => { 21794 if (flyingElement.parentNode) { 21795 flyingElement.parentNode.removeChild(flyingElement); 21796 } 21797 }, 850); 21798 } 21799 21800 // Sticky compare bar functions removed - using only fly-to-navbar animation 21801 21802 /** 21803 * Toggle compare sidebar 21804 */ 21805 function toggleCompareSidebar() { 21806 if (!compareSidebar) return; 21807 21808 const isOpen = compareSidebar.classList.contains('open'); 21809 21810 if (isOpen) { 21811 closeCompareSidebar(); 21812 } else { 21813 openCompareSidebar(); 21814 } 21815 } 21816 21817 /** 21818 * Open compare sidebar 21819 */ 21820 function openCompareSidebar() { 21821 if (!compareSidebar || !compareOverlay) return; 21822 21823 compareSidebar.classList.add('open'); 21824 compareOverlay.classList.add('active'); 21825 document.body.style.overflow = 'hidden'; 21826 21827 // Send Google Analytics event for compare sidebar open 21828 if (typeof gtag === 'function') { 21829 gtag('event', 'compare_sidebar_open', { 21830 event_category: 'Product Compare', 21831 event_label: 'Compare Sidebar Opened', 21832 products_count: compareList.length, 21833 custom_map: { 21834 custom_parameter_1: 'compare_sidebar_view' 21835 } 21836 }); 21837 } 21838 } 21839 21840 /** 21841 * Close compare sidebar 21842 */ 21843 function closeCompareSidebar() { 21844 if (!compareSidebar || !compareOverlay) return; 21845 21846 compareSidebar.classList.remove('open'); 21847 compareOverlay.classList.remove('active'); 21848 document.body.style.overflow = ''; 21849 } 21850 21851 /** 21852 * Start AI analysis 21853 */ 21854 async function startAIAnalysis() { 21855 if (compareList.length < 2) { 21856 showToast('Minimal 2 produk diperlukan untuk analisis AI', 'warning'); 21857 return; 21858 } 21859 21860 if (isAnalyzing) return; 21861 21862 // Send Google Analytics event for AI Analysis start 21863 if (typeof gtag === 'function') { 21864 gtag('event', 'ai_analysis_start', { 21865 event_category: 'Product Compare', 21866 event_label: 'AI Analysis Button Click', 21867 products_count: compareList.length, 21868 product_ids: compareList.map(p => p.id).join(','), 21869 custom_map: { 21870 custom_parameter_1: 'compare_ai_analysis' 21871 } 21872 }); 21873 } 21874 21875 // Check if products are in similar categories 21876 const mismatchInfo = await checkProductCategoryMismatch(); 21877 if (mismatchInfo.shouldWarn) { 21878 const categoryList = Array.from(mismatchInfo.categories).join(', '); 21879 const confirmed = await showConfirmModal({ 21880 title: 'Produk Berbeda Kategori', 21881 message: `Produk yang Anda pilih berasal dari kategori yang berbeda: ${categoryList}. AI tetap bisa menganalisis, tapi perbandingan mungkin kurang relevan. Lanjutkan analisis?`, 21882 confirmText: 'Ya, Analisis Tetap', 21883 cancelText: 'Batal', 21884 type: 'warning' 21885 }); 21886 21887 if (!confirmed) return; 21888 } 21889 21890 openModal(); 21891 showLoadingState(); 21892 isAnalyzing = true; 21893 21894 try { 21895 const response = await fetch('/api/compare/ai-analysis.php', { 21896 method: 'POST', 21897 credentials: 'same-origin' 21898 }); 21899 21900 const result = await response.json(); 21901 21902 if (!result.success) { 21903 throw new Error(result.error || 'Analisis AI gagal'); 21904 } 21905 21906 currentAnalysis = result; 21907 showAnalysisResults(result); 21908 21909 // Send Google Analytics event for successful AI Analysis 21910 if (typeof gtag === 'function') { 21911 gtag('event', 'ai_analysis_success', { 21912 event_category: 'Product Compare', 21913 event_label: 'AI Analysis Completed', 21914 products_count: compareList.length, 21915 analysis_type: 'product_comparison', 21916 custom_map: { 21917 custom_parameter_1: 'compare_ai_success' 21918 } 21919 }); 21920 } 21921 21922 } catch (error) { 21923 showErrorState(error.message); 21924 21925 // Send Google Analytics event for failed AI Analysis 21926 if (typeof gtag === 'function') { 21927 gtag('event', 'ai_analysis_error', { 21928 event_category: 'Product Compare', 21929 event_label: 'AI Analysis Failed', 21930 products_count: compareList.length, 21931 error_message: error.message, 21932 custom_map: { 21933 custom_parameter_1: 'compare_ai_error' 21934 } 21935 }); 21936 } 21937 } finally { 21938 isAnalyzing = false; 21939 } 21940 } 21941 21942 /** 21943 * Show modal 21944 */ 21945 function openModal() { 21946 if (!compareModal) return; 21947 compareModal.classList.add('open'); 21948 document.body.style.overflow = 'hidden'; 21949 } 21950 21951 /** 21952 * Close modal 21953 */ 21954 function closeModal() { 21955 if (!compareModal) return; 21956 compareModal.classList.remove('open'); 21957 document.body.style.overflow = ''; 21958 } 21959 21960 /** 21961 * Show loading state 21962 */ 21963 function showLoadingState() { 21964 const loading = document.getElementById('aiLoading'); 21965 const error = document.getElementById('aiError'); 21966 const results = document.getElementById('aiResults'); 21967 21968 if (loading) loading.style.display = 'block'; 21969 if (error) error.hidden = true; 21970 if (results) results.hidden = true; 21971 } 21972 21973 /** 21974 * Show error state 21975 */ 21976 function showErrorState(message) { 21977 const loading = document.getElementById('aiLoading'); 21978 const error = document.getElementById('aiError'); 21979 const results = document.getElementById('aiResults'); 21980 const errorMessage = document.getElementById('aiErrorMessage'); 21981 21982 if (loading) loading.style.display = 'none'; 21983 if (error) error.hidden = false; 21984 if (results) results.hidden = true; 21985 if (errorMessage) errorMessage.textContent = message; 21986 } 21987 21988 /** 21989 * Show analysis results 21990 */ 21991 function showAnalysisResults(analysis) { 21992 const loading = document.getElementById('aiLoading'); 21993 const error = document.getElementById('aiError'); 21994 const results = document.getElementById('aiResults'); 21995 21996 if (loading) loading.style.display = 'none'; 21997 if (error) error.hidden = true; 21998 if (results) results.hidden = false; 21999 22000 const data = analysis.data; 22001 22002 // Update summary 22003 const summaryElement = document.getElementById('aiSummary'); 22004 if (summaryElement) summaryElement.textContent = data.summary || ''; 22005 22006 const winnerProduct = document.getElementById('winnerProduct'); 22007 const winnerReason = document.getElementById('winnerReason'); 22008 if (winnerProduct && winnerReason && data.winner_overall) { 22009 const winner = compareList.find(p => parseInt(p.id) === parseInt(data.winner_overall)); 22010 winnerProduct.textContent = winner ? winner.name : 'Tidak diketa
22010hui'; 22011 winnerReason.textContent = data.winner_reason || ''; 22012 } 22013 22014 // Update comparison table 22015 updateComparisonTable(data.comparison_table); 22016 22017 // Update detailed analysis 22018 updateDetailedAnalysis(data.detailed_analysis); 22019 22020 // Update recommendations 22021 updateRecommendations(data.recommendations); 22022 22023 // Update buying tips 22024 updateBuyingTips(data.buying_tips); 22025 } 22026 22027 /** 22028 * Update comparison table 22029 */ 22030 function updateComparisonTable(comparisonData) { 22031 const container = document.getElementById('comparisonTable'); 22032 if (!container || !comparisonData?.categories) return; 22033 22034 // First, create product header row with names 22035 const productHeaderHtml = ` 22036 <div class="comparison-category"> 22037 <div class="category-header">Produk</div> 22038 <div class="category-products"> 22039 ${compareList.map(product => ` 22040 <div class="product-score"> 22041 <div class="product-name-header"> 22042 <strong>${escapeHtml(product.name)}</strong> 22043 </div> 22044 <div class="product-price"> 22045 ${formatPrice(product.price)} 22046 </div> 22047 </div> 22048 `).join('')} 22049 </div> 22050 </div> 22051 `; 22052 22053 // Then create comparison categories 22054 const categoriesHtml = comparisonData.categories.map(category => ` 22055 <div class="comparison-category"> 22056 <div class="category-header">${escapeHtml(category.name)}</div> 22057 <div class="category-products"> 22058 ${compareList.map(product => { 22059 const productData = category.products?.[product.id]; 22060 return ` 22061 <div class="product-score"> 22062 <div class="score-value"> 22063 <span class="score-stars">${'â '.repeat(productData?.score || 0)}${'â'.repeat(5 - (productData?.score || 0))}</span> 22064 <span>${productData?.score || 0}/5</span> 22065 </div> 22066 <div class="score-note">${escapeHtml(productData?.note || '')}</div> 22067 </div> 22068 `; 22069 }).join('')} 22070 </div> 22071 </div> 22072 `).join(''); 22073 22074 container.innerHTML = productHeaderHtml + categoriesHtml; 22075 } 22076 22077 /** 22078 * Update detailed analysis 22079 */ 22080 function updateDetailedAnalysis(analysisData) { 22081 const container = document.getElementById('detailedAnalysis'); 22082 if (!container || !analysisData) return; 22083 22084 container.innerHTML = compareList.map(product => { 22085 const productAnalysis = analysisData[product.id]; 22086 if (!productAnalysis) return ''; 22087 22088 // Use product name from API response if available, fallback to local name 22089 const productName = productAnalysis.product_name || product.name; 22090 22091 return ` 22092 <div class="product-analysis"> 22093 <h5>${escapeHtml(productName)}</h5> 22094 22095 ${productAnalysis.strengths ? ` 22096 <div class="analysis-section"> 22097 <h6 class="strengths">â Kelebihan</h6> 22098 <ul> 22099 ${productAnalysis.strengths.map(strength => 22100 `<li>${escapeHtml(strength)}</li>` 22101 ).join('')} 22102 </ul> 22103 </div> 22104 ` : ''} 22105 22106 ${productAnalysis.weaknesses ? ` 22107 <div class="analysis-section"> 22108 <h6 class="weaknesses">â Kekurangan</h6> 22109 <ul> 22110 ${productAnalysis.weaknesses.map(weakness => 22111 `<li>${escapeHtml(weakness)}</li>` 22112 ).join('')} 22113 </ul> 22114 </div> 22115 ` : ''} 22116 22117 ${productAnalysis.best_for ? ` 22118 <div class="analysis-section"> 22119 <h6 class="best-for">ð¯ Cocok Untuk</h6> 22120 <p class="best-for-text">${escapeHtml(productAnalysis.best_for)}</p> 22121 </div> 22122 ` : ''} 22123 </div> 22124 `; 22125 }).filter(Boolean).join(''); 22126 } 22127 22128 /** 22129 * Update recommendations 22130 */ 22131 function updateRecommendations(recommendations) { 22132 if (!recommendations) return; 22133 22134 const budgetEl = document.getElementById('budgetRecommendation'); 22135 const performanceEl = document.getElementById('performanceRecommendation'); 22136 const balancedEl = document.getElementById('balancedRecommendation'); 22137 22138 if (budgetEl && recommendations.budget_conscious) { 22139 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.budget_conscious)); 22140 budgetEl.textContent = product ? product.name : 'Tidak tersedia'; 22141 } 22142 22143 if (performanceEl && recommendations.performance_focused) { 22144 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.performance_focused)); 22145 performanceEl.textContent = product ? product.name : 'Tidak tersedia'; 22146 } 22147 22148 if (balancedEl && recommendations.balanced) { 22149 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.balanced)); 22150 balancedEl.textContent = product ? product.name : 'Tidak tersedia'; 22151 } 22152 } 22153 22154 /** 22155 * Check if products are from different categories 22156 */ 22157 async function checkProductCategoryMismatch() { 22158 if (compareList.length < 2) { 22159 return { shouldWarn: false, categories: new Set() }; 22160 } 22161 22162 // Get the root categories from each product 22163 const rootCategories = new Set(); 22164 const productCategories = []; 22165 22166 for (const product of compareList) { 22167 // Extract root category from the product's category_path if available 22168 const rootCategory = await getProductRootCategory(product); 22169 if (rootCategory) { 22170 rootCategories.add(rootCategory); 22171 productCategories.push({ 22172 product: product.name, 22173 category: rootCategory 22174 }); 22175 } 22176 } 22177 22178 // If we have more than 1 different root categories, show warning 22179 const shouldWarn = rootCategories.size > 1 && compareList.length >= 2; 22180 22181 return { 22182 shouldWarn: shouldWarn, 22183 categories: rootCategories, 22184 productCategories: productCategories 22185 }; 22186 } 22187 22188 /** 22189 * Get product root category from category_path or fallback to name detection 22190 */ 22191 async function getProductRootCategory(product) { 22192 // First try to get from category_path if available 22193 if (product.category_path) { 22194 const rootCategory = product.category_path.split('/')[0]; 22195 return rootCategory; 22196 } 22197 22198 // If no category_path, try to fetch from product detail or use name detection 22199 const detectedCategory = detectProductCategory(product.name); 22200 return detectedCategory; 22201 } 22202 22203 /** 22204 * Detect product category from name (fallback method) 22205 */ 22206 function detectProductCategory(productName) { 22207 const name = productName.toLowerCase(); 22208 22209 // Map keywords to actual root categories from the API structure 22210 const categoryMappings = { 22211 // Komputer & Laptop 22212 'Komputer & Laptop': ['laptop', 'notebook', 'macbook', 'thinkpad', 'gaming laptop', 'desktop', 'pc', 'mini pc', 'all-in-one', 'server'], 22213 22214 // Printer & Scanner 22215 'Printer & Scanner': ['printer', 'epson printer', 'canon printer', 'hp printer', 'brother printer', 'scanner', 'laser printer', 'inkjet', 'dot matrix'], 22216 22217 // Gadget 22218 'Gadget': ['smartphone', 'iphone', 'samsung phone', 'xiaomi', 'oppo', 'vivo', 'realme', 'tablet', 'ipad', 'android tablet'], 22219 22220 // Periferal
22221 'Periferal': ['monitor', 'lcd monitor', 'led monitor', 'gaming monitor', 'keyboard', 'mouse', 'gaming keyboard', 'mechanical keyboard', 'webcam'], 22222 22223 // Audio 22224 'Audio': ['headset', 'headphone', 'earphone', 'gaming headset', 'speaker', 'mikrofon', 'sound card'], 22225 22226 // Jaringan 22227 'Jaringan': ['router', 'wifi', 'access point', 'switch', 'modem', 'network'], 22228 22229 // Penyimpanan 22230 'Penyimpanan': ['ssd', 'hdd', 'nvme', 'solid state', 'hard disk', 'flash drive', 'usb drive', 'memory card'], 22231 22232 // Komponen PC 22233 'Komponen PC': ['ram', 'memory', 'ddr4', 'ddr5', 'processor', 'cpu', 'intel', 'amd', 'motherboard', 'graphics card', 'gpu', 'psu', 'power supply', 'casing'], 22234 22235 // UPS & Power 22236 'UPS & Power': ['ups', 'uninterruptible', 'stabilizer', 'power supply'], 22237 22238 // Proyektor 22239 'Proyektor': ['proyektor', 'projector', 'layar proyektor'], 22240 22241 // Security System 22242 'Security System': ['cctv', 'security camera', 'surveillance'], 22243 22244 // Aksesoris 22245 'Aksesoris': ['charger laptop', 'adaptor', 'converter', 'dock', 'hub', 'kabel', 'cable', 'power bank', 'tas laptop'], 22246 22247 // Software 22248 'Software': ['software', 'lisensi', 'license', 'antivirus'], 22249 22250 // Perlengkapan Kantor 22251 'Perlengkapan Kantor': ['fingerprint', 'finger print', 'atk', 'alat tulis'] 22252 }; 22253 22254 // Check which root category matches 22255 for (const [rootCategory, keywords] of Object.entries(categoryMappings)) { 22256 for (const keyword of keywords) { 22257 if (name.includes(keyword)) { 22258 return rootCategory; 22259 } 22260 } 22261 } 22262 22263 return null; 22264 } 22265 22266 /** 22267 * Update buying tips 22268 */ 22269 function updateBuyingTips(tips) { 22270 const container = document.getElementById('buyingTips'); 22271 if (!container || !Array.isArray(tips)) return; 22272 22273 container.innerHTML = tips.map(tip => 22274 `<li>${escapeHtml(tip)}</li>` 22275 ).join(''); 22276 } 22277 22278 // Removed share and cart functions - replaced with AI disclaimer in UI 22279 22280 /** 22281 * Remove product from compare (public method) 22282 */ 22283 async function removeProduct(productId) { 22284 try { 22285 await removeFromCompare(productId); 22286 showToast('Produk dihapus dari perbandingan', 'success'); 22287 22288 // Update all compare buttons 22289 updateAllCompareButtons(); 22290 22291 } catch (error) { 22292 showToast(error.message || 'Terjadi kesalahan', 'error'); 22293 } 22294 } 22295 22296 /** 22297 * Utility functions 22298 */ 22299 function escapeHtml(text) { 22300 const div = document.createElement('div'); 22301 div.textContent = text; 22302 return div.innerHTML; 22303 } 22304 22305 function formatPrice(price) { 22306 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 22307 } 22308 22309 /** 22310 * Show confirmation modal using the existing modal.css design 22311 */ 22312 function showConfirmModal(options = {}) { 22313 return new Promise((resolve) => { 22314 const { 22315 title = 'Konfirmasi', 22316 message = 'Apakah Anda yakin?', 22317 confirmText = 'Ya', 22318 cancelText = 'Tidak', 22319 type = 'warning' 22320 } = options; 22321 22322 // Create modal HTML 22323 const modalHTML = ` 22324 <div class="modal-overlay" id="confirmModal"> 22325 <div class="modal-dialog"> 22326 <div class="modal-header"> 22327 <h3 class="modal-title"> 22328 <div class="modal-icon ${type}"> 22329 ${getModalIcon(type)} 22330 </div> 22331 ${escapeHtml(title)} 22332 </h3> 22333 </div> 22334 <div class="modal-body"> 22335 <p class="modal-message">${escapeHtml(message)}</p> 22336 <div class="modal-actions"> 22337 <button type="button" class="modal-btn modal-btn-secondary" id="confirmCancel"> 22338 ${escapeHtml(cancelText)} 22339 </button> 22340 <button type="button" class="modal-btn modal-btn-${type === 'danger' ? 'danger' : 'primary'}" id="confirmOk"> 22341 ${escapeHtml(confirmText)} 22342 </button> 22343 </div> 22344 </div> 22345 </div> 22346 </div> 22347 `; 22348 22349 // Add to DOM 22350 document.body.insertAdjacentHTML('beforeend', modalHTML); 22351 const modal = document.getElementById('confirmModal'); 22352 const cancelBtn = document.getElementById('confirmCancel'); 22353 const okBtn = document.getElementById('confirmOk'); 22354 22355 // Show modal 22356 requestAnimationFrame(() => { 22357 modal.classList.add('show'); 22358 }); 22359 22360 // Handle events 22361 function cleanup() { 22362 modal.remove(); 22363 } 22364 22365 function handleCancel() { 22366 modal.classList.remove('show'); 22367 setTimeout(() => { 22368 cleanup(); 22369 resolve(false); 22370 }, 200); 22371 } 22372 22373 function handleConfirm() { 22374 modal.classList.remove('show'); 22375 setTimeout(() => { 22376 cleanup(); 22377 resolve(true); 22378 }, 200); 22379 } 22380 22381 cancelBtn.addEventListener('click', handleCancel); 22382 okBtn.addEventListener('click', handleConfirm); 22383 22384 // Close on overlay click 22385 modal.addEventListener('click', (e) => { 22386 if (e.target === modal) { 22387 handleCancel(); 22388 } 22389 }); 22390 22391 // Close on escape 22392 document.addEventListener('keydown', function escapeHandler(e) { 22393 if (e.key === 'Escape') { 22394 document.removeEventListener('keydown', escapeHandler); 22395 handleCancel(); 22396 } 22397 }); 22398 22399 // Focus on confirm button 22400 setTimeout(() => okBtn.focus(), 100); 22401 });
22402 } 22403 22404 /** 22405 * Get icon for modal type 22406 */ 22407 function getModalIcon(type) { 22408 const icons = { 22409 danger: 'ð¨ ', 22410 warning: 'â ï¸', 22411 success: 'â ', 22412 info: 'â¹ï¸' 22413 }; 22414 return icons[type] || icons.info; 22415 } 22416 22417 22418 function showToast(message, type = 'info') { 22419 // Integration with existing toast system or create simple one 22420 22421 // Simple alert fallback - replace with proper toast system 22422 if (type === 'error') { 22423 alert('Error: ' + message); 22424 } 22425 } 22426 22427 // Inline compare preview functions removed - keeping it simple and clean 22428 22429 /** 22430 * Get current product ID from page 22431 */ 22432 function getCurrentProductId() { 22433 const button = document.getElementById('pd-compare-btn'); 22434 return button ? parseInt(button.dataset.productId) : null; 22435 } 22436 22437 // hideStickyCompareBar function removed - no longer using sticky bar 22438 22439 // Public API 22440 window.productCompare = { 22441 removeProduct: removeProduct, 22442 addToCompare: addToCompare, 22443 loadCompareList: loadCompareList, 22444 getCompareList: function() { return compareList; }, 22445 openSidebar: openCompareSidebar, 22446 startAnalysis: startAIAnalysis, 22447 // Helper function to add from external sources with variant support 22448 addProductWithVariant: async function(productId, variantId = 0) { 22449 return await addToCompare(productId, variantId); 22450 } 22451 }; 22452 22453})(); 22454 22455 22456 22457</script>
vendor: 2 bytes, line 22457
22457
22458<script data-inlined="product-card-actions.js"> 22459/** 22460 * Product Card Quick Actions 22461 * Bottom sheet menu for quick add to cart & compare 22462 * Integrates with existing product-compare.js and cart.js 22463 */ 22464 22465(function() { 22466 'use strict'; 22467 22468 let activeSheet = null; 22469 let activeProductData = null; 22470 22471 // DOM elements (will be created dynamically) 22472 let overlay = null; 22473 let bottomSheet = null; 22474 22475 /** 22476 * Initialize on DOM ready 22477 */ 22478 document.addEventListener('DOMContentLoaded', function() { 22479 createBottomSheet(); 22480 attachEventListeners(); 22481 }); 22482 22483 /** 22484 * Create bottom sheet HTML structure 22485 */ 22486 function createBottomSheet() { 22487 // Create overlay 22488 overlay = document.createElement('div'); 22489 overlay.className = 'pca-overlay'; 22490 overlay.addEventListener('click', closeBottomSheet); 22491 22492 // Create bottom sheet 22493 bottomSheet = document.createElement('div'); 22494 bottomSheet.className = 'pca-bottom-sheet'; 22495 bottomSheet.innerHTML = ` 22496 <div class="pca-sheet-handle"></div> 22497 <div class="pca-sheet-product" id="pca-product-preview"> 22498 <!-- Product info will be inserted here --> 22499 </div> 22500 <div class="pca-sheet-actions"> 22501 <button type="button" class="pca-action-item cart" id="pca-add-cart"> 22502 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22503 <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"/> 22504 </svg> 22505 <div class="pca-action-content"> 22506 <div class="pca-action-label">Tambah ke Keranjang</div> 22507 <div class="pca-action-desc">Lanjut belanja produk lain</div> 22508 </div> 22509 </button> 22510 22511 <button type="button" class="pca-action-item compare" id="pca-add-compare"> 22512 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22513 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 22514 </svg> 22515 <div class="pca-action-content"> 22516 <div class="pca-action-label">Bandingkan Produk</div> 22517 <div class="pca-action-desc">Analisis dengan AI</div> 22518 </div> 22519 <span class="pca-action-badge compare" id="pca-compare-count" style="display:none;">0</span> 22520 </button> 22521 </div> 22522 `; 22523 22524 // Append to body 22525 document.body.appendChild(overlay); 22526 document.body.appendChild(bottomSheet); 22527 22528 // Setup action buttons 22529 document.getElementById('pca-add-cart').addEventListener('click', handleAddToCart); 22530 document.getElementById('pca-add-compare').addEventListener('click', handleAddToCompare); 22531 22532 // Prevent clicks inside bottom sheet from closing it 22533 bottomSheet.addEventListener('click', function(e) { 22534 e.stopPropagation(); 22535 }); 22536 } 22537 22538 /** 22539 * Attach event listeners to all 3-dot buttons 22540 */ 22541 function attachEventListeners() { 22542 // Use event delegation for dynamically loaded product cards 22543 document.addEventListener('click', function(e) { 22544 const btn = e.target.closest('.prod-card-actions-btn'); 22545 if (btn) { 22546 e.preventDefault(); 22547 e.stopPropagation(); 22548 handleActionsButtonClick(btn); 22549 } 22550 }); 22551 22552 // Handle ESC key 22553 document.addEventListener('keydown', function(e) { 22554 if (e.key === 'Escape' && activeSheet) { 22555 closeBottomSheet(); 22556 } 22557 }); 22558 } 22559 22560 /** 22561 * Handle 3-dot button click 22562 */ 22563 function handleActionsButtonClick(button) { 22564 // Get product ID directly from button 22565 const productId = parseInt(button.dataset.productId) || 0; 22566 if (!productId) { 22567 console.error('Product ID not found on button:', button); 22568 showToast('Data produk tidak ditemukan', 'error'); 22569 return; 22570 } 22571 22572 // Find parent card for additional data 22573 const card = button.closest('.prod-card, .pa-card'); 22574 if (!card) { 22575 console.error('Product card not found'); 22576 showToast('Data produk tidak ditemukan', 'error'); 22577 return; 22578 } 22579 22580 // Extract product data from card 22581 const productData = extractProductData(card, productId); 22582 if (!productData) { 22583 showToast('Data produk tidak ditemukan', 'error'); 22584 return; 22585 } 22586 22587 activeProductData = productData; 22588 openBottomSheet(productData); 22589 } 22590 22591 /** 22592 * Extract product data from card element 22593 */ 22594 function extractProductData(card, productId) { 22595 try { 22596 // Product ID already passed from button 22597 if (!productId) { 22598 console.error('Product ID not provided'); 22599 return null; 22600 } 22601 // Product ID already passed from button 22602 if (!productId) { 22603 console.error('Product ID not provided'); 22604 return null; 22605 } 22606 22607 const id = productId; 22608 22609 // Get product name 22610 const nameEl = card.querySelector('.prod-name, .title'); 22611 const name = nameEl ? nameEl.textContent.trim() : 'Produk'; 22612 22613 // Get product image 22614 const imgEl = card.querySelector('.thumb img, img'); 22615 const image = imgEl ? imgEl.src : '/assets/img/placeholder.png'; 22616 22617 // Get product price and detect variants 22618 const priceEl = card.querySelector('.prod-price-final, .final, .prod-price'); 22619 let price = 0; 22620 let hasVariants = false; 22621 let priceDisplay = ''; 22622 22623 if (priceEl) { 22624 // Get text content and clean it 22625 let priceText = priceEl.textContent.trim(); 22626 priceDisplay = priceText; 22627 22628 // Check if it's a range (indicates product has variants) 22629 if (priceText.includes('-')) { 22630 hasVariants = true; 22631 // Take the first price for calculation 22632 priceText = priceText.split('-')[0].trim(); 22633 } 22634 22635 // Remove all non-numeric characters 22636 priceText = priceText.replace(/[^0-9]/g, ''); 22637 price = parseInt(priceText) || 0; 22638 } 22639 22640 // Get stock status 22641 const stockDot = card.querySelector('.stock-dot'); 22642 let stockStatus = 'unknown'; 22643 let stockLabel = 'Cek stok'; 22644 22645 if (stockDot) { 22646 if (stockDot.classList.contains('ready')) { 22647 stockStatus = 'in'; 22648 stockLabel = 'Stok tersedia'; 22649 } else if (stockDot.classList.contains('low')) { 22650 stockStatus = 'low'; 22651 stockLabel = 'Stok terbatas'; 22652 } else if (stockDot.classList.contains('out')) { 22653 stockStatus = 'out'; 22654 stockLabel = 'Stok habis'; 22655 } 22656 } 22657 22658 // Get category path (if available) 22659 const categoryPath = card.dataset.category || ''; 22660 22661 // Get URL 22662 let url = '#'; 22663 if (card.tagName === 'A') { 22664 url = card.href; 22665 } else { 22666 const link = card.querySelector('a[href*="/product/"]'); 22667 if (link) url = link.href; 22668 } 22669 22670 return { 22671 id: id, 22672 name: name, 22673 image: image, 22674 price: price, 22675 priceDisplay: priceDisplay, 22676 hasVariants: hasVariants, 22677 stockStatus: stockStatus, 22678 stockLabel: stockLabel, 22679 categoryPath: categoryPath, 22680 url: url 22681 }; 22682 } catch (error) { 22683 console.error('Error extracting product data:', error); 22684 return null; 22685 } 22686 } 22687 22688 /** 22689 * Open bottom sheet 22690 */ 22691 function openBottomSheet(productData) { 22692 if (!overlay || !bottomSheet) return; 22693 22694 // Update product preview 22695 updateProductPreview(productData); 22696 22697 // Update cart button based on variant status
22698 updateCartButtonForVariant(productData); 22699 22700 // Update compare button state 22701 updateCompareButtonState(productData.id); 22702 22703 // Show overlay and sheet 22704 overlay.classList.add('active'); 22705 bottomSheet.classList.add('active'); 22706 document.body.style.overflow = 'hidden'; 22707 22708 activeSheet = true; 22709 22710 // Google Analytics 22711 if (typeof gtag === 'function') { 22712 gtag('event', 'quick_actions_open', { 22713 event_category: 'Product Card', 22714 event_label: 'Quick Actions Menu Opened', 22715 product_id: productData.id, 22716 product_name: productData.name 22717 }); 22718 } 22719 } 22720 22721 /** 22722 * Close bottom sheet 22723 */ 22724 function closeBottomSheet() { 22725 if (!overlay || !bottomSheet) return; 22726 22727 overlay.classList.remove('active'); 22728 bottomSheet.classList.remove('active'); 22729 document.body.style.overflow = ''; 22730 22731 activeSheet = false; 22732 activeProductData = null; 22733 } 22734 22735 /** 22736 * Update product preview in bottom sheet 22737 */ 22738 function updateProductPreview(productData) { 22739 const preview = document.getElementById('pca-product-preview'); 22740 if (!preview) return; 22741 22742 // Use full price display (shows range for variants) 22743 const priceDisplay = productData.priceDisplay || formatPrice(productData.price); 22744 22745 preview.innerHTML = ` 22746 <img src="${escapeHtml(productData.image)}" 22747 alt="${escapeHtml(productData.name)}" 22748 class="pca-product-image" 22749 loading="lazy"> 22750 <div class="pca-product-info"> 22751 <h4 class="pca-product-name">${escapeHtml(productData.name)}</h4> 22752 <p class="pca-product-price">${escapeHtml(priceDisplay)}</p> 22753 <p class="pca-product-stock ${productData.stockStatus}">${productData.stockLabel}</p> 22754 </div> 22755 `; 22756 } 22757 22758 /** 22759 * Update cart button for variant products 22760 */ 22761 function updateCartButtonForVariant(productData) { 22762 const cartBtn = document.getElementById('pca-add-cart'); 22763 if (!cartBtn) return; 22764 22765 if (productData.hasVariants) { 22766 // Product has variants - change to "View Detail" 22767 cartBtn.dataset.hasVariants = 'true'; 22768 cartBtn.dataset.productUrl = productData.url; 22769 22770 // Update icon to eye/external 22771 const iconSvg = cartBtn.querySelector('svg'); 22772 if (iconSvg) { 22773 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"/>'; 22774 } 22775 22776 // Update text 22777 const label = cartBtn.querySelector('.pca-action-label'); 22778 const desc = cartBtn.querySelector('.pca-action-desc'); 22779 if (label) label.textContent = 'Lihat Detail Produk'; 22780 if (desc) desc.textContent = 'Pilih varian terlebih dahulu'; 22781 22782 } else { 22783 // Single product - normal cart button 22784 cartBtn.dataset.hasVariants = 'false'; 22785 22786 // Restore cart icon 22787 const iconSvg = cartBtn.querySelector('svg'); 22788 if (iconSvg) { 22789 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"/>'; 22790 } 22791 22792 // Restore text 22793 const label = cartBtn.querySelector('.pca-action-label'); 22794 const desc = cartBtn.querySelector('.pca-action-desc'); 22795 if (label) label.textContent = 'Tambah ke Keranjang'; 22796 if (desc) desc.textContent = 'Lanjut belanja produk lain'; 22797 } 22798 } 22799 22800 /** 22801 * Update compare button state 22802 */ 22803 function updateCompareButtonState(productId) { 22804 const compareBtn = document.getElementById('pca-add-compare'); 22805 const compareBadge = document.getElementById('pca-compare-count'); 22806 22807 if (!compareBtn) return; 22808 22809 // Check if product is in compare list (using global compareList if available) 22810 let isInCompare = false;
22811 let compareCount = 0; 22812 22813 // Try to get compare list from product-compare.js via window.productCompare 22814 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.getCompareList === 'function') { 22815 const compareList = window.productCompare.getCompareList(); 22816 compareCount = compareList.length; 22817 isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 22818 } else { 22819 // Fallback: check badge count 22820 const mainBadge = document.getElementById('compareCount'); 22821 if (mainBadge) { 22822 compareCount = parseInt(mainBadge.textContent) || 0; 22823 } 22824 } 22825 22826 // Update button state 22827 if (isInCompare) { 22828 compareBtn.classList.add('active'); 22829 compareBtn.querySelector('.pca-action-label').textContent = 'Hapus dari Perbandingan'; 22830 compareBtn.querySelector('.pca-action-desc').textContent = 'Sudah ada di daftar'; 22831 } else { 22832 compareBtn.classList.remove('active'); 22833 compareBtn.querySelector('.pca-action-label').textContent = 'Bandingkan Produk'; 22834 compareBtn.querySelector('.pca-action-desc').textContent = 'Analisis dengan AI'; 22835 } 22836 22837 // Update badge 22838 if (compareBadge && compareCount > 0) { 22839 compareBadge.textContent = compareCount; 22840 compareBadge.style.display = 'inline-flex'; 22841 } else if (compareBadge) { 22842 compareBadge.style.display = 'none'; 22843 } 22844 } 22845 22846 /** 22847 * Handle add to cart 22848 */ 22849 async function handleAddToCart() { 22850 if (!activeProductData) return; 22851 22852 const button = document.getElementById('pca-add-cart'); 22853 if (!button) return; 22854 22855 // If product has variants, redirect to detail page 22856 if (activeProductData.hasVariants) { 22857 window.location.href = activeProductData.url; 22858 return; 22859 } 22860 22861 // Check stock 22862 if (activeProductData.stockStatus === 'out') { 22863 showToast('Produk sedang habis', 'warning'); 22864 return; 22865 } 22866 22867 // Show loading state 22868 button.classList.add('loading'); 22869 button.disabled = true; 22870 22871 // CSRF Token Helper 22872 function getCsrfToken() { 22873 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 22874 } 22875 22876 try { 22877 // Call cart API with proper format 22878 const response = await fetch('/api/cart/add.php', { 22879 method: 'POST', 22880 headers: { 22881 'Content-Type': 'application/json', 22882 'X-CSRF-Token': getCsrfToken() 22883 }, 22884 body: JSON.stringify({ 22885 csrf_token: getCsrfToken(), 22886 product_id: activeProductData.id, 22887 qty: 1, // Use 'qty' not 'quantity' 22888 variant_id: 0, // Default variant 22889 meta: { 22890 name: activeProductData.name, 22891 image_url: activeProductData.image 22892 } 22893 }), 22894 credentials: 'same-origin' 22895 }); 22896 22897 const result = await response.json(); 22898 22899 // Check both response status and result.ok 22900 if (!response.ok || !result.ok) { 22901 throw new Error(result.error || result.message || 'Gagal menambahkan ke keranjang'); 22902 } 22903 22904 // Success animation 22905 button.classList.remove('loading'); 22906 button.classList.add('success'); 22907 22908 // Update cart badge 22909 updateCartBadge(); 22910 22911 // PlazaAnalytics tracking 22912 if (window.PlazaAnalytics) { 22913 const _slug = (activeProductData.url || '').replace(/.*\/product\//, ''); 22914 window.PlazaAnalytics.track('add_to_cart', { slug: _slug }); 22915 } 22916 22917 // Show success toast 22918 showToast('Produk ditambahkan ke keranjang', 'success'); 22919 22920 // Google Analytics 22921 if (typeof gtag === 'function') { 22922 gtag('event', 'add_to_cart', { 22923 event_category: 'Ecommerce', 22924 event_label: 'Quick Add to Cart', 22925 items: [{ 22926 item_id: activeProductData.id, 22927 item_name: activeProductData.name, 22928 price: activeProductData.price, 22929 quantity: 1 22930 }] 22931 }); 22932 } 22933 22934 // Close sheet after short delay 22935 setTimeout(() => { 22936 closeBottomSheet(); 22937 button.classList.remove('success'); 22938 button.disabled = false; 22939 }, 1000); 22940 22941 } catch (error) { 22942 console.error('Add to cart error:', error); 22943 button.classList.remove('loading'); 22944 button.disabled = false; 22945 showToast(error.message || 'Terjadi kesalahan', 'error'); 22946 } 22947 } 22948 22949 /** 22950 * Handle add to compare 22951 */ 22952 async function handleAddToCompare() { 22953 if (!activeProductData) return; 22954 22955 const button = document.getElementById('pca-add-compare'); 22956 if (!button) return; 22957 22958 // Check if already in compare 22959 const isActive = button.classList.contains('active'); 22960 22961 // Check 5-product limit before adding 22962 if (!isActive) { 22963 const mainBadge = document.getElementById('compareCount'); 22964 const currentCount = mainBadge ? parseInt(mainBadge.textContent) || 0 : 0; 22965 22966 if (currentCount >= 5) { 22967 showToast('Maksimal 5 produk untuk perbandingan', 'warning'); 22968 return; 22969 } 22970 } 22971 22972 // Show loading state 22973 button.classList.add('loading'); 22974 button.disabled = true; 22975 22976 try { 22977 if (isActive) { 22978 // Remove from compare 22979 await removeFromCompare(activeProductData.id); 22980 showToast('Produk dihapus dari perbandingan', 'success'); 22981 } else { 22982 // Add to compare 22983 await addToCompare(activeProductData.id); 22984 showToast('Produk ditambahkan ke perbandingan', 'success'); 22985 22986 // Trigger fly animation 22987 triggerFlyToCompareAnimation(); 22988 } 22989 22990 // Success animation 22991 button.classList.remove('loading'); 22992 button.classList.add('success'); 22993 22994 // Update compare button state 22995 updateCompareButtonState(activeProductData.id); 22996 22997 // Close sheet after short delay 22998 setTimeout(() => { 22999 closeBottomSheet(); 23000 button.classList.remove('success'); 23001 button.disabled = false; 23002 }, 800); 23003 23004 } catch (error) { 23005 console.error('Compare error:', error); 23006 button.classList.remove('loading'); 23007 button.disabled = false; 23008 showToast(error.message || 'Terjadi kesalahan', 'error'); 23009 } 23010 } 23011 23012 /** 23013 * Add to compare (calls existing API) 23014 */ 23015 async function addToCompare(productId) { 23016 const response = await fetch('/api/compare/add.php', { 23017 method: 'POST', 23018 headers: { 23019 'Content-Type': 'application/json', 23020 }, 23021 body: JSON.stringify({ 23022 product_id: productId, 23023 variant_id: 0 23024 }), 23025 credentials: 'same-origin' 23026 });
23027 23028 const result = await response.json(); 23029 23030 if (!result.success) { 23031 throw new Error(result.error || 'Gagal menambahkan produk'); 23032 } 23033 23034 // Trigger compare list reload if function exists 23035 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23036 await window.productCompare.loadCompareList(); 23037 } 23038 23039 return result; 23040 } 23041 23042 /** 23043 * Remove from compare (calls existing API) 23044 */ 23045 async function removeFromCompare(productId) { 23046 const response = await fetch('/api/compare/remove.php', { 23047 method: 'POST', 23048 headers: { 23049 'Content-Type': 'application/json', 23050 }, 23051 body: JSON.stringify({ 23052 product_id: productId 23053 }), 23054 credentials: 'same-origin' 23055 }); 23056 23057 const result = await response.json(); 23058 23059 if (!result.success) { 23060 throw new Error(result.error || 'Gagal menghapus produk'); 23061 } 23062 23063 // Trigger compare list reload if function exists 23064 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23065 await window.productCompare.loadCompareList(); 23066 } 23067 23068 return result; 23069 } 23070 23071 /** 23072 * Update cart badge 23073 */ 23074 function updateCartBadge() { 23075 // Try to call global cart update function if exists 23076 if (typeof window.updateCartBadge === 'function') { 23077 window.updateCartBadge(); 23078 } else { 23079 // Fallback: increment badge manually 23080 const badge = document.getElementById('cartCount'); 23081 if (badge) { 23082 const currentCount = parseInt(badge.textContent) || 0; 23083 badge.textContent = currentCount + 1; 23084 badge.classList.add('active'); 23085 } 23086 } 23087 } 23088 23089 /** 23090 * Trigger fly to compare animation 23091 */ 23092 function triggerFlyToCompareAnimation() { 23093 const compareBtn = document.getElementById('compareBtn'); 23094 if (!compareBtn || !activeProductData) return; 23095 23096 // Create flying element 23097 const flyingElement = document.createElement('div'); 23098 flyingElement.className = 'flying-product'; 23099 flyingElement.style.cssText = ` 23100 position: fixed; 23101 width: 60px; 23102 height: 60px; 23103 z-index: 99999; 23104 pointer-events: none; 23105 `; 23106 23107 flyingElement.innerHTML = ` 23108 <img src="${activeProductData.image}" 23109 style="width:100%;height:100%;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);"> 23110 `; 23111 23112 // Position at bottom sheet 23113 const sheetRect = bottomSheet.getBoundingClientRect(); 23114 const targetRect = compareBtn.getBoundingClientRect(); 23115 23116 flyingElement.style.top = `${sheetRect.top + 20}px`; 23117 flyingElement.style.left = `${sheetRect.left + 20}px`; 23118 23119 document.body.appendChild(flyingElement); 23120 23121 // Animate to target 23122 requestAnimationFrame(() => { 23123 flyingElement.style.transition = 'all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; 23124 flyingElement.style.top = `${targetRect.top + 10}px`; 23125 flyingElement.style.left = `${targetRect.left + 10}px`; 23126 flyingElement.style.transform = 'scale(0.3)'; 23127 flyingElement.style.opacity = '0'; 23128 }); 23129 23130 // Clean up 23131 setTimeout(() => { 23132 if (flyingElement.parentNode) { 23133 flyingElement.parentNode.removeChild(flyingElement); 23134 } 23135 }, 800); 23136 } 23137 23138 /** 23139 * Format price as Rupiah 23140 */ 23141 function formatPrice(price) { 23142 if (!price || price === 0) return 'Rp 0'; 23143 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 23144 } 23145 23146 /** 23147 * Escape HTML 23148 */ 23149 function escapeHtml(text) { 23150 const div = document.createElement('div'); 23151 div.textContent = text; 23152 return div.innerHTML; 23153 } 23154 23155 /** 23156 * Show toast notification (uses global showToast if available) 23157 */ 23158 function showToast(message, type = 'info') { 23159 if (typeof window.showToast === 'function') { 23160 window.showToast({ 23161 title: type === 'success' ? 'Berhasil' : type === 'error' ? 'Gagal' : 'Info', 23162 message: message, 23163 type: type, 23164 duration: 3000 23165 }); 23166 } else { 23167 // Fallback: console log 23168 console.log(`[${type.toUpperCase()}] ${message}`); 23169 } 23170 } 23171 23172 /** 23173 * Expose functions for external access 23174 */ 23175 window.ProductCardActions = {
23176 openSheet: openBottomSheet, 23177 closeSheet: closeBottomSheet, 23178 updateCompareState: updateCompareButtonState 23179 }; 23180 23181})(); 23182 23183</script>
23183 23184
23184<script data-inlined="product-reviews.js"> 23185/** 23186 * Product Reviews - Frontend JavaScript 23187 * Handles review display, submission, and helpful voting 23188 */ 23189 23190const ProductReviews = (function() { 23191 'use strict'; 23192 23193 let currentPage = 1; 23194 let currentSort = 'helpful'; 23195 let productId = null; 23196 let isLoading = false; 23197 23198 /** 23199 * Public API: Initialize reviews for a product 23200 */ 23201 function init(pid) { 23202 productId = pid; 23203 23204 if (!productId) { 23205 console.warn('Product ID not provided to ProductReviews.init()'); 23206 return; 23207 } 23208 23209 initializeReviewTab(); 23210 loadReviews(); 23211 } 23212 23213 /** 23214 * Initialize review tab functionality 23215 */ 23216 function initializeReviewTab() { 23217 // Tab switching 23218 const tabButtons = document.querySelectorAll('.tab-btn'); 23219 tabButtons.forEach(btn => { 23220 btn.addEventListener('click', function() { 23221 const tabName = this.dataset.tab; 23222 switchTab(tabName); 23223 }); 23224 }); 23225 23226 // Sort dropdown 23227 const sortSelect = document.getElementById('review-sort'); 23228 if (sortSelect) { 23229 sortSelect.addEventListener('change', function() { 23230 currentSort = this.value; 23231 currentPage = 1; 23232 loadReviews(); 23233 }); 23234 } 23235 23236 // Load more button
23237 const loadMoreBtn = document.getElementById('load-more-reviews'); 23238 if (loadMoreBtn) { 23239 loadMoreBtn.addEventListener('click', function() { 23240 currentPage++; 23241 loadReviews(true); // Append mode 23242 }); 23243 } 23244 23245 // Write review button 23246 const writeReviewBtn = document.getElementById('write-review-btn'); 23247 if (writeReviewBtn) { 23248 writeReviewBtn.addEventListener('click', showReviewModal); 23249 } 23250 } 23251 23252 /** 23253 * Switch between product tabs 23254 */ 23255 function switchTab(tabName) { 23256 // Update button states 23257 document.querySelectorAll('.tab-btn').forEach(btn => { 23258 btn.classList.toggle('active', btn.dataset.tab === tabName); 23259 }); 23260 23261 // Update content visibility 23262 document.querySelectorAll('.tab-content').forEach(content => { 23263 content.style.display = content.id === `${tabName}-tab` ? 'block' : 'none'; 23264 }); 23265 } 23266 23267 /** 23268 * Load reviews from API 23269 */ 23270 async function loadReviews(append = false) { 23271 if (isLoading) return; 23272 isLoading = true; 23273 23274 const container = document.getElementById('reviews-list');
23275 const loadMoreBtn = document.getElementById('load-more-reviews'); 23276 23277 // Check if review section exists (product might not have reviews section) 23278 if (!container) { 23279 console.log('Review section not found - product may not have reviews yet'); 23280 isLoading = false; 23281 return; 23282 } 23283 23284 try { 23285 if (!append) { 23286 container.innerHTML = '<div class="review-loading">Memuat ulasan...</div>'; 23287 } else { 23288 loadMoreBtn.disabled = true; 23289 loadMoreBtn.textContent = 'Memuat...'; 23290 } 23291 23292 const response = await fetch(`/api/reviews/list.php?product_id=${productId}&sort=${currentSort}&page=${currentPage}&limit=10`); 23293 const data = await response.json(); 23294 23295 if (!data.success) { 23296 throw new Error(data.message || 'Gagal memuat ulasan'); 23297 } 23298 23299 // Update statistics (only on initial load) 23300 if (!append && data.stats) { 23301 updateReviewStats(data.stats); 23302 } 23303 23304 // Render reviews 23305 if (!append) { 23306 container.innerHTML = ''; 23307 } 23308 23309 const reviews = data.reviews || []; 23310 23311 if (reviews.length === 0 && !append) { 23312 container.innerHTML = '<div class="no-reviews">Belum ada ulasan untuk produk ini. Jadilah yang pertama!</div>'; 23313 } else { 23314 reviews.forEach(review => { 23315 container.appendChild(createReviewElement(review)); 23316 }); 23317 } 23318 23319 // Update load more button 23320 if (loadMoreBtn) { 23321 if (data.pagination.has_more) { 23322 loadMoreBtn.style.display = 'block'; 23323 loadMoreBtn.disabled = false; 23324 loadMoreBtn.textContent = 'Muat Lebih Banyak'; 23325 } else { 23326 loadMoreBtn.style.display = 'none'; 23327 } 23328 } 23329 23330 } catch (error) { 23331 console.error('Load reviews error:', error); 23332 container.innerHTML = '<div class="review-error">Gagal memuat ulasan. Silakan coba lagi.</div>'; 23333 } finally { 23334 isLoading = false; 23335 } 23336 } 23337 23338 /** 23339 * Update review statistics display 23340 */ 23341 function updateReviewStats(stats) { 23342 const avgRatingEl = document.getElementById('avg-rating'); 23343 const totalReviewsEl = document.getElementById('total-reviews'); 23344 const ratingBarsContainer = document.getElementById('rating-bars'); 23345 23346 if (avgRatingEl) { 23347 avgRatingEl.textContent = stats.average_rating.toFixed(1); 23348 } 23349 23350 if (totalReviewsEl) { 23351 totalReviewsEl.textContent = `${stats.total_reviews} ulasan`; 23352 } 23353 23354 // Update rating breakdown bars 23355 if (ratingBarsContainer) { 23356 ratingBarsContainer.innerHTML = ''; 23357 stats.rating_breakdown.forEach(item => { 23358 const bar = document.createElement('div'); 23359 bar.className = 'rating-bar'; 23360 bar.innerHTML = ` 23361 <span class="rating-label">${item.rating}â </span> 23362 <div class="bar-container"> 23363 <div class="bar-fill" style="width: ${item.percentage}%"></div> 23364 </div> 23365 <span class="rating-count">${item.count}</span> 23366 `; 23367 ratingBarsContainer.appendChild(bar); 23368 }); 23369 } 23370 23371 // Update tab count 23372 const reviewTab = document.querySelector('[data-tab="reviews"]'); 23373 if (reviewTab && stats.total_reviews > 0) { 23374 reviewTab.textContent = `Ulasan (${stats.total_reviews})`; 23375 } 23376 } 23377 23378 /** 23379 * Create review element 23380 */ 23381 function createReviewElement(review) { 23382 const div = document.createElement('div'); 23383 div.className = 'review-item'; 23384 div.dataset.reviewId = review.ID; 23385 23386 // Build stars 23387 const stars = 'â '.repeat(review.RATING) + 'â'.repeat(5 - review.RATING); 23388 23389 // Build images if available 23390 let imagesHtml = ''; 23391 if (review.images && review.images.length > 0) { 23392 imagesHtml = '<div class="review-images">'; 23393 review.images.forEach(img => { 23394 imagesHtml += `<img src="${escapeHtml(img)}" alt="Review image" class="review-img" loading="lazy">`; 23395 }); 23396 imagesHtml += '</div>'; 23397 } 23398 23399 // Verified purchase badge 23400 const verifiedBadge = review.IS_VERIFIED_PURCHASE === 'Y' 23401 ? '<span class="verified-badge">â Pembelian Terverifikasi</span>' 23402 : ''; 23403 23404 // Use DISPLAY_NAME if available (for anonymous reviews), otherwise use FULL_NAME 23405 const displayName = review.DISPLAY_NAME || review.FULL_NAME || 'Anonymous'; 23406 const anonymousIndicator = review.IS_ANONYMOUS === 'Y' 23407 ? '<span class="anonymous-badge" title="Nama pengguna disamark
23407an untuk privasi">ð</span>' 23408 : ''; 23409 23410 div.innerHTML = ` 23411 <div class="review-header"> 23412 <div class="review-author"> 23413 <strong>${escapeHtml(displayName)}</strong> 23414 ${anonymousIndicator} 23415 ${verifiedBadge} 23416 </div> 23417 <div class="review-rating"> 23418 <span class="stars">${stars}</span> 23419 </div> 23420 </div> 23421 <div class="review-date">${review.created_ago}</div> 23422 <div class="review-text">${escapeHtml(review.REVIEW_TEXT).replace(/\n/g, '<br>')}</div> 23423 ${imagesHtml} 23424 <div class="review-actions"> 23425 <button class="btn-helpful" data-review-id="${review.ID}"> 23426 <span class="helpful-icon">ð</span> 23427 <span class="helpful-text">Membantu (${review.HELPFUL_COUNT || 0})</span> 23428 </button> 23429 </div> 23430 `; 23431 23432 // Add helpful button listener 23433 const helpfulBtn = div.querySelector('.btn-helpful'); 23434 helpfulBtn.addEventListener('click', () => markAsHelpful(review.ID)); 23435 23436 return div; 23437 } 23438 23439 /** 23440 * Get CSRF token from meta tag 23441 */ 23442 function getCsrfToken() { 23443 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23444 return metaTag ? metaTag.getAttribute('content') : ''; 23445 } 23446 23447 /** 23448 * Mark review as helpful 23449 */ 23450 async function markAsHelpful(reviewId) { 23451 // Check if logged in 23452 const isLoggedIn = document.body.classList.contains('logged-in') || 23453 document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 23454 23455 if (!isLoggedIn) { 23456 showModal({ 23457 title: 'Login Diperlukan', 23458 message: 'Silakan login terlebih dahulu untuk memberikan vote pada ulasan ini.', 23459 icon: 'info', 23460 buttons: [ 23461 { text: 'Batal', className: 'modal-btn-secondary' }, 23462 { text: 'Login', className: 'modal-btn-primary', callback: () => window.location.href = '/login' } 23463 ] 23464 }); 23465 return; 23466 } 23467 23468 // Get current button element 23469 const reviewItem = document.querySelector(`[data-review-id="${reviewId}"]`); 23470 const helpfulBtn = reviewItem?.querySelector('.btn-helpful'); 23471 23472 if (!helpfulBtn) return; 23473 23474 // Disable button during request 23475 helpfulBtn.disabled = true; 23476 const originalText = helpfulBtn.querySelector('.helpful-text').textContent; 23477 23478 try { 23479 const response = await fetch('/api/reviews/helpful.php', { 23480 method: 'POST', 23481 headers: { 23482 'Content-Type': 'application/json', 23483 'X-CSRF-Token': getCsrfToken() 23484 }, 23485 credentials: 'same-origin', 23486 body: JSON.stringify({ 23487 csrf_token: getCsrfToken(), 23488 review_id: reviewId 23489 }) 23490 }); 23491 23492 const data = await response.json(); 23493 23494 if (data.success) { 23495 // Update button count immediately based on action 23496 const currentCount = parseInt(originalText.match(/\d+/)?.[0] || 0); 23497 const newCount = data.action === 'added' ? currentCount + 1 : currentCount - 1; 23498 helpfulBtn.querySelector('.helpful-text').textContent = `Membantu (${newCount})`; 23499 23500 // Show success message 23501 showToast(data.message, 'success'); 23502 23503 // Re-enable button 23504 helpfulBtn.disabled = false; 23505 } else { 23506 showToast(data.message, 'error'); 23507 helpfulBtn.disabled = false; 23508 } 23509 23510 } catch (error) { 23511 console.error('Mark helpful error:', error); 23512 showToast('Gagal memberikan vote. Silakan coba lagi.', 'error'); 23513 helpfulBtn.disabled = false; 23514 } 23515 } 23516 23517 /** 23518 * Show review submission modal (placeholder) 23519 */ 23520 function showReviewModal() { 23521 // TODO: Implement review modal 23522 // For now, redirect to account page 23523 window.location.href = '/account/orders'; 23524 } 23525 23526 /** 23527 * Show toast notification 23528 */ 23529 function showToast(message, type = 'info') { 23530 const toast = document.createElement('div'); 23531 toast.className = `review-toast toast-${type}`; 23532 toast.textContent = message; 23533 23534 document.body.appendChild(toast); 23535 23536 setTimeout(() => { 23537 toast.classList.add('show'); 23538 }, 100); 23539 23540 setTimeout(() => { 23541 toast.classList.remove('show'); 23542 setTimeout(() => toast.remove(), 300); 23543 }, 3000); 23544 } 23545 23546 /** 23547 * Show modal dialog 23548 * @param {Object} options - Modal options 23549 * @param {string} options.title - Modal title 23550 * @param {string} options.message - Modal message 23551 * @param {string} options.icon - Icon type (info, success, warning, danger) 23552 * @param {Array} options.buttons - Array of button objects {text, className, callback} 23553 */ 23554 function showModal(options) { 23555 const {
23556 title = 'Konfirmasi', 23557 message = '', 23558 icon = 'info', 23559 buttons = [{ text: 'OK', className: 'modal-btn-primary' }] 23560 } = options; 23561 23562 // Icon emoji mapping 23563 const iconEmojis = { 23564 info: 'â¹ï¸', 23565 success: 'â', 23566 warning: 'â ï¸', 23567 danger: 'â' 23568 }; 23569 23570 // Create modal HTML 23571 const modalHTML = ` 23572 <div class="modal-overlay" id="helpful-vote-modal" style="display: flex !important; align-items: center !important; justify-content: center !important;"> 23573 <div class="modal-dialog"> 23574 <div class="modal-header"> 23575 <h3 class="modal-title"> 23576 <span class="modal-icon ${icon}">${iconEmojis[icon] || iconEmojis.info}</span> 23577 ${escapeHtml(title)} 23578 </h3> 23579 </div> 23580 <div class="modal-body"> 23581 <p class="modal-message">${escapeHtml(message)}</p> 23582 <div class="modal-actions"> 23583 ${buttons.map((btn, index) => 23584 `<button class="modal-btn ${btn.className || 'modal-btn-secondary'}" data-index="${index}">${escapeHtml(btn.text)}</button>` 23585 ).join('')} 23586 </div> 23587 </div> 23588 </div> 23589 </div> 23590 `; 23591 23592 // Append to body 23593 const tempDiv = document.createElement('div'); 23594 tempDiv.innerHTML = modalHTML; 23595 const modal = tempDiv.firstElementChild; 23596 document.body.appendChild(modal); 23597 23598 // Add event listeners to buttons 23599 buttons.forEach((btn, index) => { 23600 const btnElement = modal.querySelector(`[data-index="${index}"]`); 23601 btnElement.addEventListener('click', () => { 23602 closeModal(modal); 23603 if (btn.callback && typeof btn.callback === 'function') { 23604 btn.callback(); 23605 } 23606 }); 23607 }); 23608 23609 // Close on overlay click 23610 modal.addEventListener('click', (e) => { 23611 if (e.target === modal) { 23612 closeModal(modal); 23613 } 23614 }); 23615 23616 // Show modal with animation 23617 setTimeout(() => modal.classList.add('show'), 10); 23618 } 23619 23620 /** 23621 * Close modal dialog 23622 */ 23623 function closeModal(modal) { 23624 modal.classList.remove('show'); 23625 setTimeout(() => modal.remove(), 200); 23626 } 23627 23628 /** 23629 * Escape HTML for XSS protection 23630 */ 23631 function escapeHtml(text) { 23632 const div = document.createElement('div'); 23633 div.textContent = text; 23634 return div.innerHTML; 23635 } 23636 23637 /** 23638 * Get product ID from URL 23639 */ 23640 function getProductIdFromUrl() { 23641 const match = window.location.pathname.match(/\/product\/(\d+)/); 23642 return match ? match[1] : null; 23643 } 23644 23645 // Public API 23646 return { 23647 init: init 23648 }; 23649 23650})(); 23651 23652</script>
23652 23653
23653<script data-inlined="review-submission.js"> 23654/** 23655 * Review Submission Modal & Handler 23656 * Handles review form display, validation, and submission 23657 */ 23658 23659(function() { 23660 'use strict'; 23661 23662 let currentProductId = null; 23663 let currentOrderId = null; 23664 let currentSoRefId = null; 23665 let selectedRating = 0; 23666 let uploadedImages = []; 23667 23668 // Get CSRF token from meta tag 23669 function getCsrfToken() { 23670 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23671 return metaTag ? metaTag.getAttribute('content') : ''; 23672 } 23673 23674 // Initialize 23675 function init() { 23676 createReviewModal(); 23677 attachEventListeners(); 23678 loadReviewableOrders(); 23679 } 23680 23681 // Create review modal HTML 23682 function createReviewModal() { 23683 const modalHTML = ` 23684 <div class="modal-overlay" id="review-modal-overlay" style="display:none;"> 23685 <div class="modal-content review-modal"> 23686 <div class="modal-header"> 23687 <h3 class="modal-title">Tulis Ulasan</h3> 23688 <button type="button" class="modal-close" id="review-modal-close" aria-label="Tutup"> 23689 <svg width="20" height="20" viewBox="0 0 24 24"> 23690 <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"/> 23691 </svg> 23692 </button> 23693 </div> 23694 23695 <div class="modal-body"> 23696 <form id="review-form" class="review-form"> 23697 <!-- Product Info --> 23698 <div class="review-product-info" id="review-product-info"> 23699 <img id="review-product-image" src="" alt="" class="review-product-thumb"> 23700 <div class="review-product-details"> 23701 <h4 id="review-product-name"></h4> 23702 <p id="review-product-sku"></p> 23703 </div> 23704 </div> 23705 23706 <!-- Rating --> 23707 <div class="form-group"> 23708 <label class="form-label">Rating <span style="color:#ef4444;">*</span></label> 23709 <div class="star-rating-input" id="star-rating-input"> 23710 <button type="button" class="star-btn" data-rating="1">â </button> 23711 <button type="button" class="star-btn" data-rating="2">â </button> 23712 <button type="button" class="star-btn" data-rating="3">â </button> 23713 <button type="button" class="star-btn" data-rating="4">â </button> 23714 <button type="button" class="star-btn" data-rating="5">â </button> 23715 </div> 23716 <p class="rating-label" id="rating-label">Pilih rating Anda</p> 23717 </div> 23718 23719 <!-- Review Text --> 23720 <div class="form-group"> 23721 <label class="form-label" for="review-text">Ulasan Anda <span style="color:#ef4444;">*</span></label> 23722 <textarea 23723 id="review-text" 23724 name="review_text" 23725 class="form-control" 23726 rows="6" 23727 placeholder="Ceritakan pengalaman Anda dengan produk ini..." 23728 minlength="20" 23729 maxlength="5000" 23730 required></textarea> 23731 <p class="form-hint"> 23732 <span id="review-char-count">0</span>/5000 karakter
23733 <span style="color:#059669;font-weight:500;"> 23734 ð° +1,000 point untuk ulasan ⥠100 karakter 23735 </span> 23736 </p> 23737 </div> 23738 23739 <!-- Image Upload --> 23740 <div class="form-group"> 23741 <label class="form-label">Upload Foto (Opsional)</label> 23742 <div class="image-upload-container"> 23743 <input type="file" 23744 id="review-images" 23745 name="images[]" 23746 accept="image/jpeg,image/jpg,image/png,image/webp" 23747 multiple 23748 style="display:none;"> 23749 <button type="button" class="btn-upload-trigger" id="upload-trigger"> 23750 <svg width="24" height="24" viewBox="0 0 24 24"> 23751 <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"/> 23752 </svg> 23753 <span>Pilih Foto</span> 23754 </button> 23755 <p class="form-hint"> 23756 Max 5 foto, format JPG/PNG/WebP, max 2MB per foto 23757 <span style="color:#059669;font-weight:500;"> 23758 ð° +2,000 point untuk ulasan dengan foto 23759 </span> 23760 </p> 23761 </div> 23762 <div class="image-preview-grid" id="image-preview-grid"></div> 23763 </div> 23764 23765 <!-- Point Reward Info --> 23766 <div class="point-reward-info"> 23767 <div class="pri-header"> 23768 <span>ð</span> 23769 <span>Dapatkan Point Reward</span> 23770 </div> 23771 <ul class="pri-list"> 23772 <li>â Ulasan dasar: <strong>5,000 point</strong></li> 23773 <li id="pri-long-text" style="opacity:0.5;">ð Ulasan panjang (â¥100 kata): <strong>+1,000 point</strong></li> 23774 <li id="pri-images" style="opacity:0.5;">ð¸ Dengan foto: <strong>+2,000 point</strong></li> 23775 </ul> 23776 <div class="pri-total"> 23777 Total Reward: <strong id="pri-total-points">5,000 point</strong> 23778 </div> 23779 </div> 23780 23781 <!-- Privacy Option --> 23782 <div class="form-group" style="margin-top:20px;padding:15px;background:#f8fafc;border-radius:8px;"> 23783 <label class="checkbox-label" style="display:flex;align-items:center;cursor:pointer;margin:0;"> 23784 <input type="checkbox" 23785 id="review-anonymous" 23786 name="is_anonymous" 23787 style="margin-right:8px;width:18px;height:18px;cursor:pointer;"> 23788 <span style="font-size:14px;"> 23789 <strong>ð Sembunyikan nama saya</strong> 23790 <span style="color:#64748b;display:block;font-size:13px;margin-top:4px;" id="review-anonymous-preview"> 23791 Nama Anda akan ditampilkan de
23791ngan disamarkan untuk menjaga privasi 23792 </span> 23793 </span> 23794 </label> 23795 </div> 23796 23797 <!-- Error Alert --> 23798 <div class="form-error" id="review-form-error" style="display:none;"></div> 23799 23800 <!-- Submit Buttons --> 23801 <div class="modal-footer"> 23802 <button type="button" class="btn btn-secondary" id="review-cancel">Batal</button> 23803 <button type="submit" class="btn btn-primary" id="review-submit"> 23804 Kirim Ulasan 23805 </button> 23806 </div> 23807 </form> 23808 </div> 23809 </div> 23810 </div> 23811 `; 23812 23813 document.body.insertAdjacentHTML('beforeend', modalHTML); 23814 } 23815 23816 // Attach event listeners 23817 function attachEventListeners() { 23818 // Modal triggers - event delegation 23819 document.addEventListener('click', function(e) { 23820 const btn = e.target.closest('.btn-write-review'); 23821 if (btn) { 23822 e.preventDefault(); 23823 openReviewModal( 23824 btn.dataset.productId, 23825 btn.dataset.orderId, 23826 btn.dataset.productName, 23827 btn.dataset.productImage, 23828 btn.dataset.productSku, 23829 btn.dataset.soRefId || null 23830 ); 23831 } 23832 }); 23833 23834 // Close modal 23835 setTimeout(() => { 23836 const closeBtn = document.getElementById('review-modal-close'); 23837 const cancelBtn = document.getElementById('review-cancel'); 23838 const overlay = document.getElementById('review-modal-overlay'); 23839 23840 if (closeBtn) { 23841 closeBtn.addEventListener('click', closeReviewModal); 23842 } 23843 if (cancelBtn) { 23844 cancelBtn.addEventListener('click', closeReviewModal); 23845 } 23846 if (overlay) { 23847 overlay.addEventListener('click', function(e) { 23848 if (e.target === overlay) closeReviewModal(); 23849 }); 23850 } 23851 }, 100); 23852 23853 // Star rating 23854 setTimeout(() => { 23855 const starButtons = document.querySelectorAll('.star-btn'); 23856 starButtons.forEach(btn => { 23857 btn.addEventListener('click', function(e) { 23858 e.preventDefault(); 23859 const rating = parseInt(this.dataset.rating); 23860 setRating(rating); 23861 }); 23862 }); 23863 }, 100); 23864 23865 // Character count 23866 setTimeout(() => { 23867 const textarea = document.getElementById('review-text'); 23868 if (textarea) { 23869 textarea.addEventListener('input', function() { 23870 updateCharCount(); 23871 calculatePotentialPoints(); 23872 }); 23873 } 23874 }, 100); 23875 23876 // Image upload 23877 setTimeout(() => { 23878 const uploadTrigger = document.getElementById('upload-trigger'); 23879 const fileInput = document.getElementById('review-images'); 23880 23881 if (uploadTrigger && fileInput) { 23882 uploadTrigger.addEventListener('click', () => fileInput.click()); 23883 fileInput.addEventListener('change', handleImageSelection); 23884 } 23885 }, 100); 23886 23887 // Form submit 23888 setTimeout(() => { 23889 const form = document.getElementById('review-form'); 23890 if (form) { 23891 form.addEventListener('submit', handleSubmit); 23892 } 23893 }, 100); 23894 } 23895 23896 // Open review modal 23897 function openReviewModal(productId, orderId, productName, productImage, productSku, soRefId = null) { 23898 const overlay = document.getElementById('review-modal-overlay'); 23899 if (!overlay) { 23900 console.error('Modal overlay not found!'); 23901 return; 23902 } 23903 23904 currentProductId = productId; 23905 currentOrderId = orderId; 23906 currentSoRefId = soRefId; 23907 selectedRating = 0; 23908 uploadedImages = []; 23909 23910 // Set product info 23911 const nameEl = document.getElementById('review-product-name'); 23912 const imageEl = document.getElementById('review-product-image'); 23913 const skuEl = document.getElementById('review-product-sku'); 23914 23915 if (nameEl) nameEl.textContent = productName; 23916 if (imageEl) imageEl.src = productImage || '/assets/img/no-image.png'; 23917 if (skuEl) skuEl.textContent = 'SKU: ' + productSku; 23918 23919 // Update privacy preview with actual user name 23920 updatePrivacyPreview(); 23921 23922 // Reset form 23923 const form = document.getElementById('review-form'); 23924 const previewGrid = document.getElementById('image-preview-grid'); 23925 const errorEl = document.getElementById('review-form-error'); 23926 23927 if (form) form.reset(); 23928 if (previewGrid) previewGrid.innerHTML = ''; 23929 if (errorEl) errorEl.style.display = 'none'; 23930 23931 setRating(0); 23932 updateCharCount(); 23933 calculatePotentialPoints();
23934 23935 // Show modal 23936 overlay.style.display = 'flex'; 23937 overlay.classList.add('show'); 23938 document.body.style.overflow = 'hidden'; 23939 } 23940 23941 // Close review modal 23942 function closeReviewModal() { 23943 const overlay = document.getElementById('review-modal-overlay'); 23944 if (overlay) { 23945 overlay.classList.remove('show'); 23946 setTimeout(() => { 23947 overlay.style.display = 'none'; 23948 }, 200); // Wait for fade animation 23949 } 23950 document.body.style.overflow = ''; 23951 } 23952 23953 // Set star rating 23954 function setRating(rating) { 23955 selectedRating = rating; 23956 const stars = document.querySelectorAll('.star-btn'); 23957 const labels = ['', 'Sangat Buruk', 'Buruk', 'Cukup', 'Bagus', 'Sangat Bagus']; 23958 23959 stars.forEach((star, index) => { 23960 if (index < rating) { 23961 star.classList.add('active'); 23962 } else { 23963 star.classList.remove('active'); 23964 } 23965 }); 23966 23967 document.getElementById('rating-label').textContent = labels[rating] || 'Pilih rating Anda'; 23968 } 23969 23970 // Update character count 23971 function updateCharCount() { 23972 const textarea = document.getElementById('review-text'); 23973 const count = textarea.value.length; 23974 document.getElementById('review-char-count').textContent = count; 23975 } 23976 23977 // Calculate potential points 23978 function calculatePotentialPoints() { 23979 const textarea = document.getElementById('review-text'); 23980 if (!textarea) return; 23981 23982 const charCount = textarea.value.trim().length; 23983 const hasImages = uploadedImages.length > 0; 23984 23985 let totalPoints = 5000; 23986 let longTextActive = charCount >= 100; 23987 let imagesActive = hasImages; 23988 23989 if (longTextActive) totalPoints += 1000; 23990 if (imagesActive) totalPoints += 2000; 23991 23992 // Update UI 23993 const longTextEl = document.getElementById('pri-long-text'); 23994 const imagesEl = document.getElementById('pri-images'); 23995 const totalEl = document.getElementById('pri-total-points'); 23996 23997 if (longTextEl) longTextEl.style.opacity = longTextActive ? '1' : '0.5'; 23998 if (imagesEl) imagesEl.style.opacity = imagesActive ? '1' : '0.5'; 23999 if (totalEl) totalEl.textContent = totalPoints.toLocaleString('id-ID') + ' point'; 24000 } 24001 24002 // Handle image selection 24003 function handleImageSelection(e) { 24004 const files = Array.from(e.target.files); 24005 24006 if (files.length > 5) { 24007 showError('Maksimal 5 foto yang dapat diupload'); 24008 e.target.value = ''; // Reset input 24009 return; 24010 } 24011 24012 uploadedImages = []; 24013 const previewGrid = document.getElementById('image-preview-grid'); 24014 previewGrid.innerHTML = ''; 24015 24016 files.forEach((file, index) => { 24017 // Validate file type 24018 if (!file.type.match(/image\/(jpeg|jpg|png|webp)/)) { 24019 showError('Format file harus JPG, PNG, atau WebP'); 24020 return; 24021 } 24022 24023 // Validate file size (2MB) 24024 if (file.size > 2 * 1024 * 1024) { 24025 showError('Ukuran file maksimal 2MB'); 24026 return; 24027 } 24028 24029 uploadedImages.push(file); 24030 24031 // Create preview 24032 const reader = new FileReader(); 24033 reader.onload = function(e) { 24034 const previewHTML = ` 24035 <div class="image-preview-item"> 24036 <img src="${e.target.result}" alt="Preview ${index + 1}"> 24037 <button type="button" class="remove-image" data-index="${index}" aria-label="Hapus"> 24038 <svg width="16" height="16" viewBox="0 0 24 24"> 24039 <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"/> 24040 </svg> 24041 </button> 24042 </div> 24043 `; 24044 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24045 };
24046 reader.readAsDataURL(file); 24047 }); 24048 24049 calculatePotentialPoints(); 24050 24051 // Attach remove handlers 24052 setTimeout(() => { 24053 document.querySelectorAll('.remove-image').forEach(btn => { 24054 btn.addEventListener('click', function() { 24055 const index = parseInt(this.dataset.index); 24056 removeImage(index); 24057 }); 24058 }); 24059 }, 100); 24060 } 24061 24062 // Remove image 24063 function removeImage(index) { 24064 uploadedImages.splice(index, 1); 24065 24066 const previewGrid = document.getElementById('image-preview-grid'); 24067 previewGrid.innerHTML = ''; 24068 24069 uploadedImages.forEach((file, idx) => { 24070 const reader = new FileReader(); 24071 reader.onload = function(e) { 24072 const previewHTML = ` 24073 <div class="image-preview-item"> 24074 <img src="${e.target.result}" alt="Preview ${idx + 1}"> 24075 <button type="button" class="remove-image" data-index="${idx}" aria-label="Hapus"> 24076 <svg width="16" height="16" viewBox="0 0 24 24"> 24077 <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"/> 24078 </svg> 24079 </button> 24080 </div> 24081 `; 24082 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24083 }; 24084 reader.readAsDataURL(file); 24085 }); 24086 24087 calculatePotentialPoints(); 24088 24089 setTimeout(() => { 24090 document.querySelectorAll('.remove-image').forEach(btn => { 24091 btn.addEventListener('click', function() { 24092 const idx = parseInt(this.dataset.index); 24093 removeImage(idx); 24094 }); 24095 }); 24096 }, 100); 24097 } 24098 24099 // Handle form submission 24100 async function handleSubmit(e) { 24101 e.preventDefault(); 24102 24103 // Validate 24104 if (selectedRating === 0) { 24105 showError('Silakan pilih rating terlebih dahulu'); 24106 return; 24107 } 24108 24109 const reviewText = document.getElementById('review-text').value.trim(); 24110 if (reviewText.length < 20) { 24111 showError('Ulasan minimal 20 karakter'); 24112 return; 24113 } 24114 24115 const submitBtn = document.getElementById('review-submit'); 24116 submitBtn.disabled = true; 24117 24118 try { 24119 // Step 1: Upload images if any 24120 let imageUrls = []; 24121 if (uploadedImages.length > 0) { 24122 submitBtn.textContent = 'Mengupload foto...'; 24123 imageUrls = await uploadImages(); 24124 if (!imageUrls) { 24125 // Upload failed 24126 submitBtn.disabled = false; 24127 submitBtn.textContent = 'Kirim Ulasan'; 24128 return; 24129 } 24130 } 24131 24132 // Step 2: Submit review with image URLs 24133 submitBtn.textContent = 'Mengirim ulasan...'; 24134 24135 const payload = { 24136 csrf_token: getCsrfToken(), 24137 product_id: currentProductId, 24138 order_id: currentOrderId || 0, 24139 so_ref_id: currentSoRefId || undefined, 24140 rating: selectedRating, 24141 review_text: reviewText, 24142 is_anonymous: document.getElementById('review-anonymous').checked, 24143 images: imageUrls 24144 }; 24145 24146 const response = await fetch('/api/reviews/submit.php', { 24147 method: 'POST', 24148 headers: { 24149 'Content-Type': 'application/json', 24150 'X-CSRF-Token': getCsrfToken() 24151 }, 24152 body: JSON.stringify(payload) 24153 }); 24154 24155 const result = await response.json(); 24156 24157 if (result.success) { 24158 showSuccess(result.message || 'Ulasan berhasil dikirim!'); 24159 closeReviewModal(); 24160 24161 // Refresh reviewable orders if on account page 24162 if (typeof loadReviewableOrders === 'function') { 24163 setTimeout(loadReviewableOrders, 500); 24164 } 24165 } else { 24166 showError(result.message || 'Gagal mengirim ulasan'); 24167 } 24168 } catch (error) { 24169 console.error('Submit error:', error); 24170 showError('Terjadi kesalahan saat mengirim ulasan'); 24171 } finally { 24172 submitBtn.disabled = false; 24173 submitBtn.textContent = 'Kirim Ulasan'; 24174 } 24175 } 24176 24177 /** 24178 * Upload images to server 24179 * @returns {Promise<Array|null>} Array of image URLs or null if failed 24180 */ 24181 async function uploadImages() { 24182 if (uploadedImages.length === 0) { 24183 return []; 24184 } 24185 24186 const formData = new FormData();
24187 uploadedImages.forEach(file => { 24188 formData.append('images[]', file); 24189 }); 24190 24191 try { 24192 const response = await fetch('/api/reviews/upload_images.php', { 24193 method: 'POST', 24194 headers: { 24195 'X-CSRF-Token': getCsrfToken() 24196 }, 24197 body: formData 24198 }); 24199 24200 // Check if response is JSON 24201 const contentType = response.headers.get('content-type'); 24202 if (!contentType || !contentType.includes('application/json')) { 24203 const text = await response.text(); 24204 console.error('Non-JSON response:', text); 24205 showError('Server error: Response bukan JSON. Cek browser console untuk detail.'); 24206 return null; 24207 } 24208 24209 const result = await response.json(); 24210 24211 if (result.success && result.images) { 24212 // Extract URLs from uploaded images 24213 return result.images.map(img => img.url); 24214 } else { 24215 showError(result.message || 'Gagal mengupload foto'); 24216 return null; 24217 } 24218 } catch (error) { 24219 console.error('Upload images error:', error); 24220 showError('Terjadi kesalahan saat mengupload foto. Cek console untuk detail.'); 24221 return null; 24222 } 24223 } 24224 24225 // Load reviewable orders (for account page) 24226 async function loadReviewableOrders() { 24227 // Only run if on account/profile page 24228 const container = document.getElementById('reviewable-orders-list'); 24229 const loading = document.getElementById('reviewable-orders-loading'); 24230 const empty = document.getElementById('reviewable-orders-empty'); 24231 24232 if (!container) { 24233 return; 24234 } 24235 24236 // Show loading 24237 if (loading) loading.style.display = 'block'; 24238 if (empty) empty.style.display = 'none'; 24239 container.innerHTML = ''; 24240 24241 try { 24242 const response = await fetch('/api/reviews/reviewable.php'); 24243 const result = await response.json(); 24244 24245 if (loading) loading.style.display = 'none'; 24246 24247 if (result.success && result.data.length > 0) { 24248 renderReviewableOrders(result.data); 24249 if (empty) empty.style.display = 'none'; 24250 } else { 24251 if (empty) empty.style.display = 'block'; 24252 } 24253 } catch (error) { 24254 console.error('Load reviewable orders error:', error); 24255 if (loading) loading.style.display = 'none'; 24256 if (empty) { 24257 empty.innerHTML = '<p>Gagal memuat data. Silakan refresh halaman.</p>'; 24258 empty.style.display = 'block'; 24259 } 24260 } 24261 } 24262 24263 // Render reviewable orders 24264 function renderReviewableOrders(orders) { 24265 const container = document.getElementById('reviewable-orders-list'); 24266 if (!container) return; 24267 24268 let html = ''; 24269 24270 orders.forEach(order => { 24271 html += ` 24272 <div class="reviewable-order-item"> 24273 <img src="${escapeHtml(order.product_image || '/assets/img/no-image.png')}" alt="${escapeHtml(order.product_name)}" class="roi-image"> 24274 <div class="roi-info"> 24275 <h4>${escapeHtml(order.product_name)}</h4> 24276 <p class="roi-meta">Order: ${escapeHtml(order.order_number)}</p> 24277 <p class="roi-meta">Dibeli: ${escapeHtml(order.order_date)}</p> 24278 </div> 24279 <button type="button" 24280 class="btn btn-primary btn-write-review" 24281 data-product-id="${order.product_id}" 24282 data-order-id="${order.order_id || ''}" 24283 data-so-ref-id="${escapeHtml(order.so_ref_id || '')}" 24284 data-product-name="${escapeHtml(order.product_name)}" 24285 data-product-image="${escapeHtml(order.product_image || '')}" 24286 data-product-sku="${escapeHtml(order.product_sku || '')}"> 24287 âï¸ Tulis Ulasan 24288 </button> 24289 </div> 24290 `; 24291 }); 24292 24293 container.innerHTML = html; 24294 } 24295 24296 // Escape HTML for XSS protection 24297 function escapeHtml(text) { 24298 const div = document.createElement('div'); 24299 div.textContent = text; 24300 return div.innerHTML; 24301 } 24302 24303 // Show error 24304 function showError(message) { 24305 const errorEl = document.getElementById('review-form-error'); 24306 errorEl.textContent = message; 24307 errorEl.style.display = 'block'; 24308 24309 setTimeout(() => { 24310 errorEl.style.display = 'none'; 24311 }, 5000); 24312 } 24313 24314 // Show success 24315 function showSuccess(message) { 24316 // Create toast notification 24317 const toast = document.createElement('div'); 24318 toast.className = 'toast-notification success'; 24319 toast.textContent = message; 24320 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;'; 24321 24322 document.body.appendChild(toast); 24323 24324 setTimeout(() => { 24325 toast.style.opacity = '0'; 24326 toast.style.transition = 'opacity 0.3s'; 24327 setTimeout(() => toast.remove(), 300); 24328 }, 3000); 24329 } 24330 24331 // Mask user name for privacy preview (like PHP maskReviewerName) 24332 function maskUserName(fullName) { 24333 if (!fullName || fullName.trim() === '') { 24334 return 'Anonymous'; 24335 } 24336 24337 const words = fullName.trim().split(/\s+/); 24338 const maskedWords = words.map(word => { 24339 const len = word.length; 24340 if (len <= 1) return word; 24341 return word.charAt(0) + '*'.repeat(len - 1); 24342 }); 24343 24344 return maskedWords.join(' '); 24345 } 24346 24347 // Update privacy preview text with user's actual masked name 24348 function updatePrivacyPreview() { 24349 const previewEl = document.getElementById('review-anonymous-preview'); 24350 if (!previewEl) return; 24351 24352 const userName = window.userFullName || ''; 24353 if (userName) { 24354 const maskedName = maskUserName(userName); 24355 previewEl.textContent = `Nama Anda akan ditampilkan sebagai "${maskedName}" untuk menjaga privasi`; 24356 } else { 24357 previewEl.textContent = 'Nama Anda akan ditampilkan de
24357ngan disamarkan untuk menjaga privasi'; 24358 } 24359 } 24360 24361 // Initialize when DOM ready 24362 if (document.readyState === 'loading') { 24363 document.addEventListener('DOMContentLoaded', init); 24364 } else { 24365 init(); 24366 } 24367 24368 // Export openReviewModal to global scope for use in other scripts 24369 window.openReviewModal = openReviewModal; 24370 24371})(); 24372 24373</script>
24373 24374 <style> 24375/* ========================================================= 24376 â¡ FIXED FOOTER â RED ELEGANT MODE 24377========================================================= */ 24378 24379.sbo-top-fixed-footer{ 24380 position: fixed; 24381 left: 0; 24382 right: 0; 24383 bottom: 0; 24384 z-index: 999; 24385 display: flex; 24386 align-items: center; 24387 justify-content: space-around; 24388 gap: 8px; 24389 padding: 11px 12px calc(11px + env(safe-area-inset-bottom)); 24390 overflow: hidden; 24391 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%)); 24392 backdrop-filter: blur(12px); 24393 border-top: 1px solid rgb(143 1 1); 24394 border-radius: 18px 18px 0 0; 24395 box-shadow: 0 -12px 30px #410000, inset 0 1px 0 rgba(255, 255, 255, .05); 24396 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 24397} 24398 24399/* scanline */ 24400.sbo-top-fixed-footer::before{ 24401 content:""; 24402 position:absolute; 24403 inset:0; 24404 pointer-events:none; 24405 background: 24406 repeating-linear-gradient( 24407 0deg, 24408 rgba(255,255,255,.015), 24409 rgba(255,255,255,.015) 1px, 24410 transparent 1px, 24411 transparent 6px 24412 ); 24413 opacity:.85; 24414 mix-blend-mode:overlay; 24415} 24416 24417/* shine sweep */ 24418.sbo-top-fixed-footer::after{ 24419 content:""; 24420 position:absolute; 24421 top:0; 24422 left:-160%; 24423 width:60%; 24424 height:100%; 24425 background: linear-gradient(120deg, transparent, rgba(255, 90, 90, 0.25), transparent); 24426 transform: skewX(-25deg); 24427 animation: footerFinalShine 3.8s infinite; 24428 mix-blend-mode: screen; 24429 pointer-events:none; 24430} 24431 24432@keyframes footerFinalShine{ 24433 0%{left:-160%;} 24434 68%{left:160%;} 24435 100%{left:160%;} 24436} 24437 24438/* ========================================================= 24439 ITEM 24440========================================================= */ 24441 24442.sbo-top-fixed-footer a{ 24443 flex:1; 24444 max-width:110px; 24445 24446 display:flex; 24447 flex-direction:column; 24448 align-items:center; 24449 justify-content:center; 24450 gap:6px; 24451 24452 padding: 8px 6px; 24453 24454 text-decoration:none; 24455 text-align:center; 24456 24457 color: rgba(171 0 0); 24458 font-size: 12px; 24459 font-weight: 800; 24460 letter-spacing: .25px; 24461 24462 border-radius: 14px; 24463 position:relative; 24464 z-index:1; 24465 24466 transition: all .18s ease; 24467} 24468 24469.sbo-top-fixed-footer a:hover{ 24470 color:#ffffff; 24471 background: rgba(255, 80, 80, 0.08); 24472 filter: drop-shadow(0 0 12px rgba(255, 80, 80, 0.15)); 24473 transform: translateY(-1px); 24474} 24475 24476.sbo-top-fixed-footer a:active{ 24477 transform: translateY(0) scale(.99); 24478} 24479 24480/* ========================================================= 24481 ICON 24482========================================================= */ 24483 24484.sbo-top-fixed-footer img{ 24485 width:26px; 24486 height:26px; 24487 transition: .18s ease; 24488 filter: drop-shadow(0 0 8px rgba(255, 80, 80, 0.35)); 24489} 24490 24491.sbo-top-fixed-footer a:hover img{ 24492 transform: scale(1.08); 24493 filter: drop-shadow(0 0 16px rgba(255, 80, 80, 0.6)); 24494} 24495 24496/* ========================================================= 24497 HUD CORNERS 24498========================================================= */ 24499 24500.sbo-top-fixed-footer .hud-corner{ 24501 position:absolute; 24502 width:18px;height:18px; 24503 pointer-events:none; 24504 border: 1px solid rgba(255, 90, 90, 0.5); 24505 filter: drop-shadow(0 0 10px rgba(255, 80, 80, 0.2)); 24506 opacity:.85; 24507} 24508 24509.sbo-top-fixed-footer .hud-tl{top:8px;left:10px;border-right:none;border-bottom:none;border-radius:10px 0 0 0;} 24510.sbo-top-fixed-footer .hud-tr{top:8px;right:10px;border-left:none;border-bottom:none;border-radius:0 10px 0 0;} 24511.sbo-top-fixed-footer .hud-bl{bottom:8px;left:10px;border-right:none;border-top:none;border-radius:0 0 0 10px;} 24512.sbo-top-fixed-footer .hud-br{bottom:8px;right:10px;border-left:none;border-top:none;border-radius:0 0 10px 0;} 24513 24514/* ========================================================= 24515 CENTER CTA 24516========================================================= */ 24517 24518.sbo-top-fixed-footer .tada{ 24519 color: #ffffff; 24520 background: radial-gradient(circle at 50% 0%, #f52b2b, rgba(0, 0, 0, 0) 70%), rgba(255, 60, 60, 0.08); 24521 border: 1px solid rgba(255, 90, 90, 0.35); 24522 box-shadow: 0 0 18px #f52b2b, inset 0 0 16px rgba(255, 60, 60, 0.15); 24523} 24524 24525.sbo-top-fixed-footer .tada img{ 24526 width:30px; 24527 height:30px; 24528 filter: drop-shadow(0 0 16px rgba(255, 80, 80, 0.65)); 24529} 24530 24531/* pulse */ 24532.sbo-top-fixed-footer .tada::before{ 24533 content:""; 24534 position:absolute; 24535 inset:-12px; 24536 border-radius:999px; 24537 background: 24538 radial-gradient(circle, #000000, transparent 70%), 24539 radial-gradient(circle, rgba(255, 120, 120, 0.08), transparent 72%); 24540 filter: blur(12px); 24541 opacity:.55; 24542 animation: corePulse 2.9s ease-in-out infinite; 24543 z-index:-1; 24544} 24545 24546@keyframes corePulse{ 24547 0%,100%{opacity:.35; transform:scale(.99);} 24548 50%{opacity:.75; transform:scale(1.02);} 24549} 24550 24551/* ACTIVE */ 24552.sbo-top-fixed-footer a.is-active{ 24553 color:#ffffff; 24554 background: rgba(255, 80, 80, 0.1); 24555 box-shadow: inset 0 0 0 1px rgba(255, 90, 90, 0.25); 24556} 24557 24558.sbo-top-fixed-footer a.is-active img{ 24559 filter: drop-shadow(0 0 18px rgba(255, 80, 80, 0.8)); 24560} 24561 24562/* RESPONSIVE */ 24563@media (max-width:500px){ 24564 .sbo-top-fixed-footer{ 24565 padding: 10px 10px calc(10px + env(safe-area-inset-bottom)); 24566 border-radius: 16px 16px 0 0; 24567 gap:6px; 24568 } 24569 .sbo-top-fixed-footer a{ 24570 font-size:11px; 24571 padding:7px 5px; 24572 gap:5px; 24573 max-width:104px; 24574 } 24575 .sbo-top-fixed-footer img{width:22px;height:22px;} 24576 .sbo-top-fixed-footer .tada img{width:26px;height:26px;} 24577} 24578</style> 24579<div class="sbo-top-fixed-footer"> 24580 <a href="https://calcourtsfitness.nichesite.org/" target="_blank" rel="nofollow noopener"> 24581 <img src="https://a1.asdqwe.vip/assets/icon-piontogel.png" alt="BONUS PIONTOGEL"> 24582 Promosi 24583 </a> 24584 <a href="https://calcourtsfitness.nichesite.org/" target="_blank" rel="nofollow noopener"> 24585 <img src="https://a1.asdqwe.vip/assets/icon-piontogel.png" alt="LOGIN PIONTOGEL"> 24586 Login 24587 </a> 24588 <a href="https://calcourtsfitness.nichesite.org/" target="_blank" rel="nofollow noopener" class="tada"> 24589 <img src="https://a1.asdqwe.vip/assets/icon-piontogel.png" alt="DAFTAR PIONTOGEL"> 24590 Daftar 24591 </a> 24592 <a href="https://calcourtsfitness.nichesite.org/" target="_blank" rel="nofollow noopener"> 24593 <img src="https://a1.asdqwe.vip/assets/icon-piontogel.png" alt="LINK ALTERNATIF PIONTOGEL"> 24594 Alternatif
24595 </a> 24596 <a href="https://calcourtsfitness.nichesite.org/" target="_blank" rel="nofollow noopener"> 24597 <img src="https://a1.asdqwe.vip/assets/icon-piontogel.png" alt="LIVE CHAT PIONTOGEL"> 24598 Live Chat 24599 </a> 24600</div>
24601<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: 2 bytes, line 24601
24601
24602<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":"a0396214bde043d4b4e51479815a65fd","r":1}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 24602
24602
24603<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":"a0396214bde043d4b4e51479815a65fd","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 24603
24603
24604<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"0fe56ca5af564d63864d847a5bf01947","r":1,"spa":2}' crossorigin="anonymous"></script>
24604 24605</body> 24606</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.