1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <link rel="icon" href="/favicon.ico" sizes="any"> 7 <link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png"> 8 <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> 9 <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> 10 <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> 11 <link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png"> 12 <link rel="icon" type="image/png" sizes="512x512" href="/android-chrome-512x512.png"> 13 <title>Leader Fox | Traditional Czech bicycle manufacturer</title> 14 <meta name="description" content="Leader Fox â traditional Czech manufacturer of bicycles and e-bikes. Quick access to the e-shop, catalogue, contacts, showroom and dealer network."> 15 <link rel="alternate" hreflang="cs" href="https://www.leaderfox.cz/"> 16 <link rel="alternate" hreflang="en" href="https://www.leaderfox.com/"> 17 <link rel="alternate" hreflang="x-default" href="https://www.leaderfox.cz/"> 18
18<script src="https://cdn.tailwindcss.com"></script>
18 19 <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"> 20 <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/MarkerCluster.css"> 21 <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/MarkerCluster.Default.css"> 22 23 <style> 24 html { scroll-behavior: smooth; } 25 26 body { 27 background: #ffffff; 28 color: #18181b; 29 } 30 31 .hero-bg { 32 background-image: 33 linear-gradient(to bottom, rgba(8, 11, 18, 0.22) 0%, rgba(8, 11, 18, 0.12) 28%, rgba(8, 11, 18, 0.56) 100%), 34 url('hlavni-fotka.jpg'); 35 background-size: cover; 36 background-position: center top; 37 background-repeat: no-repeat; 38 } 39 40 .nav-blur { 41 background: rgba(10, 10, 10, 0.20); 42 backdrop-filter: blur(14px); 43 -webkit-backdrop-filter: blur(14px); 44 } 45 46 .section-line { 47 background: linear-gradient(90deg, rgba(234,179,8,1) 0%, rgba(234,179,8,0.35) 100%); 48 height: 3px; 49 width: 84px; 50 } 51 52 .card-lift { 53 transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease; 54 } 55 56 .card-lift:hover { 57 transform: translateY(-4px); 58 box-shadow: 0 24px 60px rgba(0,0,0,0.10); 59 } 60 61 .hero-title-shadow { 62 text-shadow: 0 10px 30px rgba(0,0,0,0.30); 63 } 64 65 .soft-ring { 66 box-shadow: 0 0 0 1px rgba(255,255,255,0.18) inset; 67 } 68 69 #dealer-map-frame { 70 height: 620px; 71 width: 100%; 72 display: block; 73 background: #f4f4f5; 74 z-index: 1; 75 } 76 77 .dealer-map-popup { 78 font-family: Arial, sans-serif; 79 min-width: 220px; 80 } 81 82 .dealer-map-popup-title { 83 font-weight: 900; 84 font-size: 15px; 85 color: #18181b; 86 margin-bottom: 6px; 87 } 88 89 .dealer-map-popup-head { 90 display: flex; 91 align-items: center; 92 justify-content: space-between; 93 gap: 10px; 94 margin-bottom: 6px; 95 } 96 97 .dealer-map-popup-strip { 98 width: 56px; 99 height: 7px; 100 border-radius: 999px; 101 background: var(--partner-strip-color, #86efac); 102 flex: 0 0 auto; 103 } 104 105 .dealer-map-popup-text { 106 font-size: 13px; 107 color: #52525b; 108 line-height: 1.35; 109 } 110 111 .dealer-map-popup-link { 112 display: inline-block; 113 margin-top: 8px; 114 color: #ca8a04; 115 font-weight: 800; 116 text-decoration: none; 117 } 118 119 .marker-cluster-small, 120 .marker-cluster-medium, 121 .marker-cluster-large { 122 background-color: rgba(234, 179, 8, 0.28); 123 } 124 125 .marker-cluster-small div, 126 .marker-cluster-medium div, 127 .marker-cluster-large div { 128 background-color: rgba(234, 179, 8, 0.88); 129 color: #111827; 130 font-weight: 900; 131 } 132 133 .marker-cluster.cluster-gold { 134 background-color: rgba(250, 204, 21, 0.32); 135 } 136 137 .marker-cluster.cluster-gold div { 138 background-color: rgba(250, 204, 21, 0.96); 139 color: #18181b; 140 border: 2px solid #ffffff; 141 box-shadow: 0 10px 28px rgba(250, 204, 21, 0.34); 142 } 143 144 .marker-cluster.cluster-silver { 145 background-color: rgba(212, 212, 216, 0.38); 146 } 147 148 .marker-cluster.cluster-silver div { 149 background-color: rgba(212, 212, 216, 0.96); 150 color: #18181b; 151 border: 2px solid #ffffff; 152 box-shadow: 0 10px 28px rgba(113, 113, 122, 0.28); 153 } 154 155 .marker-cluster.cluster-bronze { 156 background-color: rgba(180, 83, 9, 0.32); 157 } 158 159 .marker-cluster.cluster-bronze div { 160 background-color: rgba(180, 83, 9, 0.96); 161 color: #ffffff; 162 border: 2px solid #ffffff; 163 box-shadow: 0 10px 28px rgba(180, 83, 9, 0.28); 164 } 165 166 167 .dealer-card-active { 168 border-color: rgb(234 179 8) !important; 169 box-shadow: 0 12px 30px rgba(234,179,8,0.18); 170 background: rgba(254, 252, 232, 0.9); 171 } 172 173 .dealer-link { 174 color: #2563eb; 175 text-decoration: underline; 176 text-underline-offset: 2px; 177 } 178 179 .dealer-link:hover { 180 color: #1d4ed8; 181 } 182 183 184 185 .reveal { 186 opacity: 0; 187 transform: translateY(26px); 188 transition: opacity 0.75s ease, transform 0.75s ease; 189 } 190 191 .reveal.is-visible { 192 opacity: 1; 193 transform: translateY(0); 194 } 195 196 .hero-bg { 197 background-attachment: scroll; 198 } 199 200 .premium-surface { 201 background: 202 radial-gradient(circle at 15% 10%, rgba(234,179,8,0.16), transparent 30%), 203 radial-gradient(circle at 85% 70%, rgba(255,255,255,0.06), transparent 28%), 204 linear-gradient(135deg, #050505 0%, #111113 52%, #030303 100%); 205 } 206 207 .category-card { 208 min-height: 360px; 209 position: relative; 210 overflow: hidden; 211 isolation: isolate; 212 } 213 214 .category-card::before { 215 content: ''; 216 position: absolute; 217 inset: 0; 218 background: 219 linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.74)), 220 radial-gradient(circle at 50% 35%, rgba(234,179,8,0.24), transparent 34%); 221 opacity: 0.92; 222 transition: transform 0.45s ease, opacity 0.45s ease; 223 z-index: -1; 224 } 225 226 .category-card::after { 227 content: ''; 228 position: absolute; 229 inset: 1px; 230 border-radius: 1.5rem; 231 border: 1px solid rgba(255,255,255,0.10); 232 pointer-events: none; 233 } 234 235 .category-card:hover::before { 236 transform: scale(1.08); 237 opacity: 1; 238 } 239 240 .category-number { 241 font-size: clamp(3.5rem, 8vw, 7rem); 242 line-height: 0.8; 243 letter-spacing: -0.08em; 244 } 245 246 .floating-cta { 247 display: none; 248 } 249 250 251 252 .premium-badge { 253 display: inline-flex; 254 align-items: center; 255 gap: 0.35rem; 256 border-radius: 999px; 257 padding: 0.28rem 0.55rem; 258 font-size: 10px; 259 font-weight: 900; 260 letter-spacing: 0.12em; 261 text-transform: uppercase; 262 } 263 264 .premium-badge.gold { 265 background: #facc15; 266 color: #18181b; 267 } 268 269 .premium-badge.silver { 270 background: #e5e7eb; 271 color: #18181b; 272 } 273 274 .premium-badge.bronze { 275 background: #b45309; 276 color: #ffffff; 277 } 278 279 .dealer-card-premium { 280 border-color: rgba(234,179,8,0.72) !important; 281 box-shadow: 0 16px 42px rgba(234,179,8,0.13); 282 } 283 284 .dealer-card { 285 position: relative; 286 overflow: hidden; 287 } 288 289 .dealer-card::before { 290 content: ''; 291 position: absolute; 292 left: 0; 293 top: 0; 294 bottom: 0; 295 width: 7px; 296 background: var(--partner-strip-color, #86efac); 297 } 298 299 .premium-map-marker { 300 border-radius: 999px; 301 border: 2px solid #ffffff; 302 box-shadow: 0 7px 18px rgba(0,0,0,0.22); 303 } 304 305 .dealer-type-icon { 306 width: 38px; 307 height: 38px; 308 border-radius: 999px; 309 display: flex; 310 align-items: center; 311 justify-content: center; 312 background: #2563eb; 313 color: #ffffff; 314 border: 3px solid #ffffff; 315 box-shadow: 0 10px 26px rgba(0,0,0,0.30); 316 font-size: 20px; 317 line-height: 1; 318 } 319 320 .dealer-type-icon.service { 321 background: #18181b; 322 } 323 324 .dealer-type-icon.pickup { 325 background: #16a34a; 326 } 327 328 .dealer-type-icon.gold { 329 background: #facc15; 330 color: #18181b; 331 border-color: #ffffff; 332 } 333 334 .dealer-type-icon.silver { 335 background: #d4d4d8; 336 color: #18181b; 337 border-color: #ffffff; 338 } 339 340 .dealer-type-icon.bronze { 341 background: #b45309; 342 color: #ffffff; 343 border-color: #ffffff; 344 } 345 346 .dealer-type-icon-wrap { 347 background: transparent; 348 border: 0; 349 } 350 351 .czech-region-tooltip { 352 background: rgba(24,24,27,0.88); 353 border: 1px solid rgba(250,204,21,0.38); 354 border-radius: 999px; 355 color: #ffffff; 356 font-size: 11px; 357 font-weight: 800; 358 letter-spacing: 0.04em; 359 padding: 3px 8px; 360 box-shadow: 0 8px 24px rgba(0,0,0,0.22); 361 } 362 363 .gallery-tab.is-active { 364 background: #facc15; 365 color: #18181b; 366 border-color: #facc15; 367 } 368 369 .gallery-thumb { 370 position: relative; 371 display: block; 372 overflow: hidden; 373 border-radius: 1.5rem; 374 background: #18181b; 375 cursor: pointer; 376 min-height: 230px; 377 } 378 379 .gallery-thumb img { 380 width: 100%; 381 height: 100%; 382 min-height: 230px; 383 object-fit: cover; 384 display: block; 385 transform: scale(1.01); 386 transition: transform 0.35s ease, opacity 0.35s ease; 387 } 388 389 .gallery-thumb:hover img { 390 transform: scale(1.06); 391 } 392 393 .gallery-thumb::after { 394 content: ''; 395 position: absolute; 396 inset: 0; 397 background: linear-gradient(180deg, transparent 45%, rgba(0,0,0,0.55)); 398 opacity: 0; 399 transition: opacity 0.35s ease; 400 } 401 402 .gallery-thumb:hover::after { 403 opacity: 1; 404 } 405 406 .gallery-lightbox { 407 position: fixed; 408 inset: 0; 409 z-index: 100; 410 display: none; 411 align-items: center; 412 justify-content: center; 413 background: rgba(0,0,0,0.92); 414 padding: 24px; 415 } 416 417 .gallery-lightbox.is-open { 418 display: flex; 419 } 420 421 .gallery-lightbox img { 422 max-width: min(1200px, 92vw); 423 max-height: 86vh; 424 object-fit: contain; 425 box-shadow: 0 24px 80px rgba(0,0,0,0.65); 426 } 427 428 .gallery-lightbox-button { 429 position: absolute; 430 border: 1px solid rgba(255,255,255,0.35); 431 background: rgba(255,255,255,0.08); 432 color: #ffffff; 433 width: 44px; 434 height: 44px; 435 border-radius: 999px; 436 font-size: 26px; 437 font-weight: 900; 438 line-height: 1; 439 display: flex; 440 align-items: center; 441 justify-content: center; 442 transition: background 0.2s ease, color 0.2s ease; 443 } 444 445 .gallery-lightbox-button:hover { 446 background: #facc15; 447 color: #18181b; 448 } 449 450 .gallery-lightbox-close { 451 top: 22px; 452 right: 22px; 453 } 454 455 .gallery-lightbox-prev { 456 left: 22px; 457 top: 50%; 458 transform: translateY(-50%); 459 } 460 461 .gallery-lightbox-next { 462 right: 22px; 463 top: 50%; 464 transform: translateY(-50%); 465 } 466 467 .gallery-lightbox-caption { 468 position: absolute; 469 left: 24px; 470 right: 24px; 471 bottom: 18px; 472 text-align: center; 473 color: rgba(255,255,255,0.82); 474 font-size: 13px; 475 font-weight: 800; 476 letter-spacing: 0.16em; 477 text-transform: uppercase; 478 } 479 480 481 482 483 @media (max-width: 767px) { 484 .hero-bg { 485 background-attachment: scroll; 486 } 487 488 .floating-cta { 489 display: grid; 490 grid-template-columns: 1fr 1fr; 491 gap: 8px; 492 position: fixed; 493 left: 12px; 494 right: 12px; 495 bottom: 12px; 496 z-index: 60; 497 padding: 8px; 498 border-radius: 18px; 499 background: rgba(10,10,10,0.72); 500 backdrop-filter: blur(14px); 501 -webkit-backdrop-filter: blur(14px); 502 box-shadow: 0 20px 45px rgba(0,0,0,0.28); 503 } 504 505 body { 506 padding-bottom: 78px; 507 } 508 } 509 510 @media (max-width: 1279px) { 511 #dealer-map-frame { 512 height: 520px; 513 } 514 } 515 516 @media (max-width: 767px) { 517 #dealer-map-frame { 518 height: 420px; 519 } 520 } 521 </style> 522</head> 523<body class="font-sans antialiased"> 524 525 <!-- HERO --> 526 <section class="hero-bg min-h-screen flex flex-col relative overflow-hidden"> 527 <header class="sticky top-0 z-30"> 528 <div class="nav-blur border-b border-white/10"> 529 <div class="max-w-7xl mx-auto px-5 md:px-8 py-4 flex items-center justify-end"> 530 <nav class="hidden md:flex items-center gap-7 text-white text-[12px] font-bold uppercase tracking-[0.24em]"> 531 <a href="https://e-shop.leaderfox.com" class="hover:text-yellow-300 transition">E-shop</a>
532 <a href="#galerie" class="hover:text-yellow-300 transition">Gallery</a> 533 <a href="https://e-shop.leaderfox.com/dokument/katalog/" class="hover:text-yellow-300 transition">Catalogue</a> 534 <a href="#kategorie" class="hover:text-yellow-300 transition">Categories</a> 535 <a href="#rozcestnik" class="hover:text-yellow-300 transition">Quick links</a> 536 <a href="#partneri" class="hover:text-yellow-300 transition">Dealers</a> 537 <a href="coverage-map.html" class="hover:text-yellow-300 transition">Coverage map</a> 538 <a href="https://e-shop.leaderfox.com/dokument/kontakt/" class="hover:text-yellow-300 transition">Contact</a> 539 <span class="inline-flex items-center gap-2 rounded-full border border-white/25 px-3 py-1 text-[11px] tracking-[0.16em]"> 540 <a href="index.html" class="hover:text-yellow-300 transition">CZ</a> 541 <span class="text-white/40">|</span> 542 <span class="text-yellow-300">EN</span> 543 </span> 544 </nav> 545 </div> 546 </div> 547 </header> 548 549 <div class="flex-1 relative"> 550 <div class="absolute left-0 bottom-0 w-full px-3 md:px-4 pb-4 md:pb-6"> 551 <div class="max-w-3xl"> 552 <div class="inline-flex items-center gap-3 mb-5 px-4 py-2 rounded-full bg-white/10 text-white text-[11px] md:text-xs uppercase tracking-[0.28em] font-bold soft-ring"> 553 <span class="h-2 w-2 rounded-full bg-yellow-400"></span> 554 Traditional Czech manufacturer of bicycles and e-bikes 555 </div> 556 557 <h1 class="text-white text-5xl sm:text-6xl md:text-8xl font-semibold italic tracking-tight leading-[0.95] hero-title-shadow"> 558 the good ride 559 </h1> 560 561 <div class="mt-8 flex flex-col sm:flex-row gap-4"> 562 <a href="https://e-shop.leaderfox.com" 563 class="bg-yellow-400 hover:bg-yellow-300 text-black font-black py-4 px-8 uppercase tracking-[0.18em] text-sm transition shadow-2xl text-center"> 564 Go to e-shop 565 </a> 566 <a href="#partneri" 567 class="border border-white/70 text-white hover:bg-white hover:text-black font-black py-4 px-8 uppercase tracking-[0.18em] text-sm transition text-center"> 568 Where to buy and service 569 </a> 570 </div> 571 </div> 572 </div> 573 </div> 574 </section> 575 576 <!-- GALERIE --> 577 <section id="galerie" class="bg-white py-20 md:py-24 reveal"> 578 <div class="max-w-7xl mx-auto px-5 md:px-8"> 579 <div class="max-w-3xl mb-10"> 580 <div class="section-line mb-6"></div> 581 <p class="text-xs md:text-sm uppercase tracking-[0.35em] font-black text-yellow-500 mb-4"> 582 Gallery 583 </p> 584 585 </div> 586 587 <div class="flex flex-wrap gap-3 mb-8"> 588 <button type="button" class="gallery-tab is-active rounded-full border border-zinc-300 px-5 py-3 text-xs font-black uppercase tracking-[0.22em] text-zinc-800 transition" data-gallery-set="hero"> 589 Hero 590 </button> 591 <button type="button" class="gallery-tab rounded-full border border-zinc-300 px-5 py-3 text-xs font-black uppercase tracking-[0.22em] text-zinc-800 hover:border-yellow-400 transition" data-gallery-set="alps"> 592 Alps 593 </button> 594 </div> 595 596 <div id="gallery-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5"></div> 597 598 <div class="text-center mt-10"> 599 <button id="gallery-load-more" type="button" class="inline-flex bg-zinc-900 text-white hover:bg-yellow-400 hover:text-black font-black py-4 px-8 uppercase tracking-[0.18em] text-sm transition"> 600 Load more photos 601 </button> 602 </div> 603 </div> 604 </section> 605 606 <div id="gallery-lightbox" class="gallery-lightbox" aria-hidden="true"> 607 <button type="button" id="gallery-lightbox-close" class="gallery-lightbox-button gallery-lightbox-close" aria-label="Close gallery">Ã</button> 608 <button type="button" id="gallery-lightbox-prev" class="gallery-lightbox-button gallery-lightbox-prev" aria-label="Previous photo">â¹</button> 609 <img id="gallery-lightbox-image" src="" alt=""> 610 <button type="button" id="gallery-lightbox-next" class="gallery-lightbox-button gallery-lightbox-next" aria-label="Next p
610hoto">âº</button> 611 <div id="gallery-lightbox-caption" class="gallery-lightbox-caption"></div> 612 </div> 613 614 <!-- INTRO / O ZNACCE --> 615 <section class="bg-white py-20 md:py-24 reveal"> 616 <div class="max-w-7xl mx-auto px-5 md:px-8 grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-center"> 617 <div class="lg:col-span-7"> 618 <div class="section-line mb-6"></div> 619 <p class="text-xs md:text-sm uppercase tracking-[0.35em] font-black text-yellow-500 mb-4"> 620 Czech tradition since 1996 621 </p> 622 <h2 class="text-3xl md:text-5xl font-black tracking-tight text-zinc-900 leading-tight mb-6"> 623 Bicycles from Äeské BudÄjovice for the city, sport and everyday riding. 624 </h2> 625 <p class="text-zinc-600 text-base md:text-lg leading-relaxed max-w-3xl"> 626 Leader Fox builds on its own development, careful assembly and proven components. 627 Here you can find the most important links to the e-shop, catalogues, showroom, contacts 628 and the partner network of dealers and service points. 629 </p> 630 </div> 631 632 <div class="lg:col-span-5"> 633 <div class="grid grid-cols-2 gap-4"> 634 <div class="rounded-3xl border border-zinc-200 bg-zinc-50 p-6 md:p-8"> 635 <div class="text-3xl md:text-4xl font-black text-zinc-900">25+</div> 636 <div class="mt-2 text-[11px] uppercase tracking-[0.24em] font-bold text-zinc-500">Years of the brand</div> 637 </div> 638 <div class="rounded-3xl border border-zinc-900 bg-zinc-900 p-6 md:p-8"> 639 <div class="text-3xl md:text-4xl font-black text-white">100%</div> 640 <div class="mt-2 text-[11px] uppercase tracking-[0.24em] font-bold text-zinc-400">Focus on own assembly</div> 641 </div> 642 </div> 643 </div> 644 </div> 645 </section> 646 647 <!-- KATEGORIE E-SHOPU --> 648 <section id="kategorie" class="premium-surface text-white py-20 md:py-28 reveal"> 649 <div class="max-w-7xl mx-auto px-5 md:px-8"> 650 <div class="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-end mb-12"> 651 <div class="lg:col-span-7"> 652 <div class="section-line mb-6"></div> 653 <p class="text-xs md:text-sm uppercase tracking-[0.35em] font-black text-yellow-400 mb-4"> 654 E-shop categories 655 </p> 656 <h2 class="text-3xl md:text-6xl font-black tracking-tight leading-[0.95]"> 657 Quick access directly to the Leader Fox range. 658 </h2> 659 </div> 660 <div class="lg:col-span-5"> 661 </div> 662 </div> 663 664 <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5"> 665 <a href="https://e-shop.leaderfox.com/elektrokola-menu-1R30000101.aspx" class="category-card rounded-3xl bg-zinc-900 p-7 flex flex-col justify-between group"> 666 <div class="flex items-start justify-between gap-4"> 667 <div class="text-xl font-bold text-white mb-3">Power for longer rides.</div> 668 <div class="category-number font-black text-white/10 group-hover:text-yellow-400/20 transition">01</div> 669 </div> 670 <div> 671 <h3 class="text-4xl md:text-5xl font-black tracking-tight mb-4 text-yellow-400">E-bikes</h3> 672 <p class="text-zinc-300 text-sm leading-relaxed mb-6">Mountain, trekking, city, folding, fat bike and other e-bikes.</p> 673 <span class="inline-flex text-sm font-black uppercase tracking-[0.18em] text-white">View e-bikes â</span> 674 </div> 675 </a> 676 677 <a href="https://e-shop.leaderfox.com/jizdni-kola-menu-G510000101.aspx" class="category-card rounded-3xl bg-zinc-900 p-7 flex flex-col justify-between group"> 678 <div class="flex items-start justify-between gap-4"> 679 <div class="text-xl font-bold text-white mb-3">Classic bikes for sport and trips.</div> 680 <div class="category-number font-black text-white/10 group-hover:text-yellow-400/20 transition">02</div> 681 </div> 682 <div> 683 <h3 class="text-4xl md:text-5xl font-black tracking-tight mb-4 text-yellow-400">Bicycles</h3> 684 <p class="text-zinc-300 text-sm leading-relaxed mb-6">Mountain, cross, trekking and childrenâs bikes by riding style.</p>
685 <span class="inline-flex text-sm font-black uppercase tracking-[0.18em] text-white">View bicycles â</span> 686 </div> 687 </a> 688 689 <a href="https://e-shop.leaderfox.com/nove-modely-menu-6WG1000101.aspx" class="category-card rounded-3xl bg-zinc-900 p-7 flex flex-col justify-between group"> 690 <div class="flex items-start justify-between gap-4"> 691 <div class="text-xl font-bold text-white mb-3">Current collection.</div> 692 <div class="category-number font-black text-white/10 group-hover:text-yellow-400/20 transition">03</div> 693 </div> 694 <div> 695 <h3 class="text-4xl md:text-5xl font-black tracking-tight mb-4 text-yellow-400">New models</h3> 696 <p class="text-zinc-300 text-sm leading-relaxed mb-6">New e-bikes, bicycles, folding bikes and childrenâs bikes.</p> 697 <span class="inline-flex text-sm font-black uppercase tracking-[0.18em] text-white">View new models â</span> 698 </div> 699 </a> 700 701 <a href="https://e-shop.leaderfox.com/cargo-e-kola-menu-1E20000101.aspx" class="category-card rounded-3xl bg-zinc-900 p-7 flex flex-col justify-between group"> 702 <div class="flex items-start justify-between gap-4"> 703 <div class="text-xl font-bold text-white mb-3">For family and cargo.</div> 704 <div class="category-number font-black text-white/10 group-hover:text-yellow-400/20 transition">04</div> 705 </div> 706 <div> 707 <h3 class="text-4xl md:text-5xl font-black tracking-tight mb-4 text-yellow-400">Cargo e-bikes</h3> 708 <p class="text-zinc-300 text-sm leading-relaxed mb-6">Practical e-bikes for everyday transport, children and larger loads.</p> 709 <span class="inline-flex text-sm font-black uppercase tracking-[0.18em] text-white">View cargo e-bikes â</span> 710 </div> 711 </a> 712 713 <a href="https://e-shop.leaderfox.com/kola-s-vadou-laku-menu-GWJ0000101.aspx" class="category-card rounded-3xl bg-zinc-900 p-7 flex flex-col justify-between group"> 714 <div class="flex items-start justify-between gap-4"> 715 <div class="text-xl font-bold text-white mb-3">Special offer.</div> 716 <div class="category-number font-black text-white/10 group-hover:text-yellow-400/20 transition">05</div> 717 </div> 718 <div> 719 <h3 class="text-4xl md:text-5xl font-black tracking-tight mb-4 text-yellow-400">Bikes with paint defects</h3> 720 <p class="text-zinc-300 text-sm leading-relaxed mb-6">E-bikes and bicycles with cosmetic paint defects at a better price.</p> 721 <span class="inline-flex text-sm font-black uppercase tracking-[0.18em] text-white">View bikes with paint defects â</span> 722 </div> 723 </a> 724 725 <a href="https://e-shop.leaderfox.com/nahradni-dily-a-merch-menu-4WJ0000101.aspx" class="category-card rounded-3xl bg-zinc-900 p-7 flex flex-col justify-between group"> 726 <div class="flex items-start justify-between gap-4"> 727 <div class="text-xl font-bold text-white mb-3">Bike accessories.</div> 728 <div class="category-number font-black text-white/10 group-hover:text-yellow-400/20 transition">06</div> 729 </div> 730 <div> 731 <h3 class="text-4xl md:text-5xl font-black tracking-tight mb-4 text-yellow-400">Spare parts and merch</h3> 732 <p class="text-zinc-300 text-sm leading-relaxed mb-6">Batteries, chargers, brakes, tyres, stands, merch and more.</p> 733 <span class="inline-flex text-sm font-black uppercase tracking-[0.18em] text-white">View spare parts and merch â</span> 734 </div> 735 </a> 736 </div> 737 </div> 738 </section> 739 740 <!-- ROZCESTNIK --> 741 <section id="rozcestnik" class="bg-zinc-950 text-white py-20 md:py-24 reveal"> 742 <div class="max-w-7xl mx-auto px-5 md:px-8"> 743 <div class="flex flex-col md:flex-row md:items-end md:justify-between gap-6 mb-10"> 744 <div> 745 <div class="section-line mb-6"></div> 746 <p class="text-xs md:text-sm uppercase tracking-[0.35em] font-black text-yellow-400 mb-4"> 747 Quick links 748 </p> 749 <h2 class="text-3xl md:text-5xl font-black tracking-tight leading-tight"> 750 Everything important in one place. 751 </h2> 752 </div> 753 754 </div> 755 756 <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-
7565"> 757 <a href="https://e-shop.leaderfox.com" 758 class="card-lift rounded-3xl border border-white/10 bg-white/5 p-7 hover:border-yellow-400/60 hover:bg-white/10 block"> 759 <div class="text-[11px] uppercase tracking-[0.26em] font-black text-yellow-400 mb-4">01</div> 760 <h3 class="text-2xl font-black tracking-tight mb-3">E-shop</h3> 761 <p class="text-zinc-300 text-sm leading-relaxed"> 762 Complete range of bicycles, e-bikes and accessories. 763 </p> 764 </a> 765 766 <a href="https://e-shop.leaderfox.com/dokument/katalog/" 767 class="card-lift rounded-3xl border border-white/10 bg-white/5 p-7 hover:border-yellow-400/60 hover:bg-white/10 block"> 768 <div class="text-[11px] uppercase tracking-[0.26em] font-black text-yellow-400 mb-4">02</div> 769 <h3 class="text-2xl font-black tracking-tight mb-3">Catalogue</h3> 770 <p class="text-zinc-300 text-sm leading-relaxed"> 771 Overview of models, technical specifications and collections. 772 </p> 773 </a> 774 775 <a href="https://e-shop.leaderfox.com/dokument/znackova-prodejna/" 776 class="card-lift rounded-3xl border border-white/10 bg-white/5 p-7 hover:border-yellow-400/60 hover:bg-white/10 block"> 777 <div class="text-[11px] uppercase tracking-[0.26em] font-black text-yellow-400 mb-4">03</div> 778 <h3 class="text-2xl font-black tracking-tight mb-3">Showroom</h3> 779 <p class="text-zinc-300 text-sm leading-relaxed"> 780 Brand store and direct personal contact in Äeské BudÄjovice. 781 </p> 782 </a> 783 784 <a href="https://e-shop.leaderfox.com/dokument/kontakt/" 785 class="card-lift rounded-3xl border border-white/10 bg-white/5 p-7 hover:border-yellow-400/60 hover:bg-white/10 block"> 786 <div class="text-[11px] uppercase tracking-[0.26em] font-black text-yellow-400 mb-4">04</div> 787 <h3 class="text-2xl font-black tracking-tight mb-3">Contact</h3> 788 <p class="text-zinc-300 text-sm leading-relaxed"> 789 Company contacts, service information and other contact details. 790 </p> 791 </a> 792 </div> 793 </div> 794 </section> 795 796 <!-- PRODEJCI A SERVIS --> 797 <section id="partneri" class="bg-zinc-50 py-20 md:py-24 border-t border-zinc-200 border-b border-zinc-200 reveal"> 798 <div class="max-w-7xl mx-auto px-5 md:px-8"> 799 <div class="max-w-3xl mb-12"> 800 <div class="section-line mb-6"></div> 801 <p class="text-xs md:text-sm uppercase tracking-[0.35em] font-black text-yellow-500 mb-4"> 802 Dealers and service 803 </p> 804 <h2 class="text-3xl md:text-5xl font-black tracking-tight text-zinc-900 leading-tight mb-6"> 805 Where to buy Leader Fox and where to get service. 806 </h2> 807 <p class="text-zinc-600 text-base md:text-lg leading-relaxed"> 808 Choose the type of place, region or country. Results are shown on the map and in a list below. 809 </p> 810 </div> 811 812 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-10"> 813 <div class="card-lift rounded-3xl bg-white border border-zinc-200 p-8"> 814 <div class="text-[11px] uppercase tracking-[0.24em] font-black text-yellow-500 mb-4">Brand store</div> 815 <h3 class="text-2xl font-black tracking-tight mb-4">Äeské BudÄjovice</h3> 816 <p class="text-zinc-600 leading-relaxed mb-8"> 817 Visit the showroom, choose models and contact the brand directly. 818 </p> 819 <a href="https://e-shop.leaderfox.com/dokument/znackova-prodejna/" 820 class="inline-block bg-zinc-900 text-white hover:bg-black font-black py-4 px-6 uppercase tracking-[0.18em] text-sm transition"> 821 Visit showroom 822 </a> 823 </div> 824 825 <div class="card-lift rounded-3xl bg-white border border-zinc-200 p-8"> 826 <div class="text-[11px] uppercase tracking-[0.24em] font-black text-yellow-500 mb-4">Partner network</div> 827 <h3 class="text-2xl font-black tracking-tight mb-4">Dealers in Czechia and abroad</h3> 828 <p class="text-zinc-600 leading-relaxed mb-8"> 829 Overview of places where you can buy Leader Fox bikes or consult your choice. 830 </p> 831 <a href="#dealer-map" 832 class="inline-block border border-zinc-900 text-zinc-900 hover:bg-zinc-900 hover:text-white font-black py-4 px-6 uppercase tracking-[0.18em] text-sm transition"> 833 Interactive map 834 </a> 835 </div> 836 837 <div class="card-lift rounded-3xl bg-zinc-900 text-white border border-zinc-900 p-8"> 838 <div class="text-[11px] uppercase tracking-[0.24em] font-black text-yellow-400 mb-4">Cooperation</div> 839 <h3 class="text-2xl font-black tracking-tight mb-4">Become a dealer</h3> 840 <p class="text-zinc-300 leading-relaxed mb-8"> 841 Are you interested in offering Leader Fox bikes or expanding the service network? 842 </p> 843 <a href="https://e-shop.leaderfox.com/webform4Dealer.aspx" 844 class="inline-block bg-yellow-400 text-black hover:bg-yellow-300 font-black py-4 px-6 uppercase tracking-[0.18em] text-sm transition"> 845 Contact form 846 </a> 847 </div> 848 </div> 849 850 <div id="dealer-map" class="rounded-3xl border border-zinc-200 bg-white overflow-hidden"> 851 <div class="p-6 md:p-8 border-b border-zinc-200"> 852 <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 ga
852p-4"> 853 854 <div> 855 <label for="dealer-country" class="block text-[11px] uppercase tracking-[0.22em] font-black text-zinc-500 mb-2">Country</label> 856 <select id="dealer-country" class="w-full rounded-2xl border border-zinc-300 px-4 py-3 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:border-yellow-400"> 857 <option value="">All countries</option> 858 </select> 859 </div> 860 861 862 863 <div> 864 <label for="dealer-scope" class="block text-[11px] uppercase tracking-[0.22em] font-black text-zinc-500 mb-2">Czech region</label> 865 <select id="dealer-scope" disabled class="w-full rounded-2xl border border-zinc-300 px-4 py-3 text-sm bg-zinc-100 text-zinc-400 cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:border-yellow-400"> 866 <option value="">All Czechia</option> 867 <option value="region:Praha">â Praha</option> 868 <option value="region:StÅedoÄeský kraj">â StÅedoÄeský kraj</option> 869 <option value="region:JihoÄeský kraj">â JihoÄeský kraj</option> 870 <option value="region:PlzeÅský kraj">â PlzeÅský kraj</option> 871 <option value="region:Karlovarský kraj">â Karlovarský kraj</option> 872 <option value="region:Ãstecký kraj">â Ãstecký kraj</option> 873 <option value="region:Liberecký kraj">â Liberecký kraj</option> 874 <option value="region:Královéhradecký kraj">â Královéhradecký kraj</option> 875 <option value="region:Pardubický kraj">â Pardubický kraj</option> 876 <option value="region:Kraj VysoÄina">â Kraj VysoÄina</option> 877 <option value="region:Jihomoravský kraj">â Jihomoravský kraj</option> 878 <option value="region:Olomoucký kraj">â Olomoucký kraj</option> 879 <option value="region:Moravskoslezský kraj">â Moravskoslezský kraj</option> 880 <option value="region:ZlÃnský kraj">â ZlÃnský kraj</option> 881 </select> 882 </div> 883 884 885 886 <div> 887 <label for="dealer-type" class="block text-[11px] uppercase tracking-[0.22em] font-black text-zinc-500 mb-2">Type of place</label> 888 <select id="dealer-type" class="w-full rounded-2xl border border-zinc-300 px-4 py-3 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:border-yellow-400"> 889 <option value="">All</option> 890 <option value="dealer">Dealers</option> 891 <option value="service">Service points</option> 892 <option value="pickup">Pickup points</option> 893 </select> 894 </div> 895 896 897 <div class="xl:col-span-2"> 898 <label for="dealer-search" class="block text-[11px] uppercase tracking-[0.22em] font-black text-zinc-500 mb-2">Search</label> 899 <input id="dealer-search" type="text" placeholder="Name, city, region, country..." class="w-full rounded-2xl border border-zinc-300 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:border-yellow-400"> 900 </div> 901 </div> 902 903 <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mt-5"> 904 <div id="dealer-count" class="text-sm text-zinc-500">Loading dataâ¦</div> 905 <button id="dealer-reset" type="button" class="text-sm font-bold text-zinc-700 hover:text-black transition text-left md:text-right"> 906 Reset filters 907 </button> 908 </div> 909 <div id="dealer-status" class="mt-3 text-sm text-zinc-500"></div> 910 </div> 911 912 <div id="dealer-map-frame" aria-label="Mapa prodejců a servisnÃch places"></div> 913 914 <div class="p-4 md:p-6 border-t border-zinc-200 bg-zinc-50 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> 915 <div class="text-sm text-zinc-600"> 916 The map shows currently filtered dealers and service points with coordinates. 917 </div> 918 <a id="dealer-map-external" href="https://www.google.com/maps?q=Czechia" target="_blank" rel="noopener" class="inline-flex items-center justify-center rounded-xl border border-zinc-900 px-4 py-2 text-sm font-black text-zinc-900 hover:bg-zinc-900 hover:text-white transition"> 919 Open in large map 920 </a> 921 </div> 922 923 <div class="p-6 md:p-8 border-t border-zinc-200"> 924 <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-6"> 925 <h3 class="text-xl font-black tracking-tight text-zinc-900">Results</h3> 926 <div class="text-sm text-zinc-500">Click the icon to zoom the map to the location detail</div> 927 </div> 928 929 <div id="dealer-list" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5"></div> 930 931 <div class="flex items-center justify-between mt-8 pt-6 border-t border-zinc-200"> 932 <button id="dealer-prev" type="button" class="rounded-xl border border-zinc-300 px-4 py-2 text-sm font-bold text-zinc-700 disabled:opacity-40 disabled:cursor-not-allowed">Previous</button> 933 <div id="dealer-page-info" class="text-sm text-zinc-500">Page 1</div> 934 <button id="dealer-next" type="button" class="rounded-xl border border-zinc-300 px-4 py-2 text-sm font-bold text-zinc-700 disabled:opacity-40 disabled:cursor-not-allowed">Next</button> 935 </div> 936 </div> 937 </div> 938 </div> 939 </section> 940 941 <!-- BLOG --> 942 <section id="blog" class="bg-white py-20 md:py-24"> 943 <div class="max-w-7xl mx-auto px-5 md:px-8"> 944 <div class="max-w-3xl mx-auto text-center mb-12"> 945 <div class="section-line mx-auto mb-6"></div> 946 <p class="text-xs md:text-sm uppercase tracking-[0.35em] font-black text-yellow-500 mb-4"> 947 News and inspiration 948 </p> 949 <h2 class="text-3xl md:text-5xl font-black tracking-tight text-zinc-900 mb-6"> 950 Follow news from the world of Leader Fox. 951 </h2> 952 <p class="text-zinc-600 text-base md:text-lg leading-relaxed"> 953 New model introductions, trip tips, brand updates and more content for cycling fans. 954 </p> 955 </div> 956 957 <div id="blog-preview" class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10"></div> 958 959 <div id="blog-fallback" class="hidden text-center mb-10"> 960 <p class="text-zinc-500 text-sm"> 961 You can find current articles directly in the e-shop blog. 962 </p> 963 </div> 964 965 <div class="text-center"> 966 <a href="https://e-shop.leaderfox.com/aktualita/" 967 class="inline-block bg-zinc-900 text-white hover:bg-yellow-400 hover:text-black font-black py-4 px-8 uppercase tracking-[0.18em] text-sm transition"> 968 Go to blog 969 </a> 970 </div> 971 </div> 972 </section> 973 974 <!-- FOOTER --> 975 <footer class="bg-black text-white"> 976 <div class="max-w-7xl mx-auto px-5 md:px-8 pt-16 pb-10"> 977 <div class="grid grid-cols-1 md:grid-cols-3 gap-10 border-b border-zinc-800 pb-12"> 978 <div> 979 <div class="text-3xl font-black uppercase italic tracking-tight leading-none">LEADER FOX</div> 980 <div class="mt-3 text-[11px] uppercase tracking-[0.35em] text-zinc-500 font-bold"> 981 the good ride 982 </div> 983 </div> 984 985 <div> 986 <div class="text-[11px] uppercase tracking-[0.26em] font-black text-zinc-500 mb-4">Quick links</div> 987 <div class="space-y-3 text-sm text-zinc-300"> 988 <div><a href="https://e-shop.leaderfox.com" class="hover:text-white transition">E-shop</a></div> 989 <div><a href="https://e-shop.leaderfox.com/dokument/katalog/" class="hover:text-white transition">Catalogue</a></div> 990 <div><a href="https://e-shop.leaderfox.com/dokument/znackova-prodejna/" class="hover:text-white transition">Showroom</a></div> 991 <div><a href="https://e-shop.leaderfox.com/dokument/kontakt/" class="hover:text-white transition">Contact</a></div> 992 </div> 993 </div> 994 995 <div> 996 <div class="text-[11px] uppercase tracking-[0.26em] font-black text-zinc-500 mb-4">For partners</div> 997 <div class="space-y-3 text-sm text-zinc-300"> 998 <div><a href="#partneri" class="hover:text-white transition">Dealers and service</a></div> 999 <div><a href="https://e-shop.leaderfox.com/webform4Dealer.aspx" class="hover:text-white transition">Become a dealer</a></div> 1000 <div><a href="https://e-shop.leaderfox.com/aktualita/" class="hover:text-white transition">Blog</a></div> 1001 </div> 1002 </div> 1003 </div> 1004 1005 <div class="pt-8 text-center text-[10px] uppercase tracking-[0.28em] text-zinc-600"> 1006 © 2026 Leader Fox | Bohemia Bike s.r.o. | Made in Czech Republic 1007 </div> 1008 </div> 1009 </footer> 1010 1011 1012 1013 <!-- MOBILNI RYCHLE VOLBY --> 1014 <div class="floating-cta" aria-label="Rychlé volby"> 1015 <a href="https://e-shop.leaderfox.com" class="rounded-xl bg-yellow-400 px-4 py-3 text-center text-xs font-black uppercase tracking-[0.16em] text-black"> 1016 E-shop 1017 </a> 1018 <a href="#partneri" class="rounded-xl border border-white/30 px-4 py-3 text-center text-xs font-black uppercase tracking-[0.16em] text-white"> 1019 Dealers 1020 </a> 1021 </div> 1022 1023 1024
1024<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
1024 1025
1025<script src="https://unpkg.com/[email protected]/dist/leaflet.markercluster.js"></script>
1025 1026 1027
1027<script> 1028 const revealObserver = new IntersectionObserver((entries) => { 1029 entries.forEach(entry => { 1030 if (entry.isIntersecting) { 1031 entry.target.classList.add('is-visible'); 1032 revealObserver.unobserve(entry.target); 1033 } 1034 }); 1035 }, { threshold: 0.12 }); 1036 1037 document.querySelectorAll('.reveal').forEach(element => revealObserver.observe(element)); 1038 1039 /* 1040 * JemnÄjšà rolovánà koleÄkem myÅ¡i. 1041 * NemÄnà obsah stránky, menu ani funkce prodejců. 1042 * Platà jen pro desktop / myÅ¡; mobilnà touch scroll zůstává nativnÃ. 1043 */ 1044 function initGentleWheelScroll() { 1045 const isDesktopPointer = window.matchMedia('(min-width: 768px) and (pointer: fine)').matches; 1046 const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1047 1048 if (!isDesktopPointer || reducedMotion) return; 1049 1050 let targetY = window.scrollY; 1051 let currentY = window.scrollY; 1052 let isAnimating = false; 1053 1054 const maxScroll = () => Math.max(0, document.documentElement.scrollHeight - window.innerHeight); 1055 1056 function normalizeWheelDelta(event) { 1057 if (event.deltaMode === 1) return event.deltaY * 16; 1058 if (event.deltaMode === 2) return event.deltaY * window.innerHeight; 1059 return event.deltaY; 1060 } 1061 1062 function animateScroll() { 1063 currentY += (targetY - currentY) * 0.22; 1064 1065 if (Math.abs(targetY - currentY) < 0.5) { 1066 currentY = targetY; 1067 isAnimating = false; 1068 } 1069 1070 window.scrollTo(0, currentY); 1071 1072 if (isAnimating) { 1073 requestAnimationFrame(animateScroll); 1074 } 1075 } 1076 1077 window.addEventListener('wheel', (event) => { 1078 const tagName = event.target && event.target.tagName ? event.target.tagName.toLowerCase() : ''; 1079 const isFormControl = ['input', 'textarea', 'select', 'option'].includes(tagName); 1080 const isEmbeddedMap = event.target && event.target.closest && event.target.closest('#dealer-map-frame'); 1081 1082 if (isFormControl || isEmbeddedMap || event.ctrlKey) return; 1083 1084 event.preventDefault(); 1085 1086 const delta = normalizeWheelDelta(event); 1087 targetY = Math.max(0, Math.min(maxScroll(), targetY + delta * 0.48)); 1088 1089 if (!isAnimating) { 1090 currentY = window.scrollY; 1091 isAnimating = true; 1092 requestAnimationFrame(animateScroll); 1093 } 1094 }, { passive: false }); 1095 1096 window.addEventListener('scroll', () => { 1097 if (!isAnimating) { 1098 targetY = window.scrollY; 1099 currentY = window.scrollY; 1100 } 1101 }, { passive: true }); 1102 } 1103 1104 initGentleWheelScroll(); 1105 1106 1107 let dealerData = []; 1108 let filteredData = []; 1109 let currentPage = 1; 1110 const PAGE_SIZE = 12; 1111 let activeCardId = null; 1112 let searchFilledFromMarker = false;
1113 1114 const $search = document.getElementById('dealer-search'); 1115 const $type = document.getElementById('dealer-type'); 1116 const $scope = document.getElementById('dealer-scope'); 1117 const $country = document.getElementById('dealer-country'); 1118 const $count = document.getElementById('dealer-count'); 1119 const $list = document.getElementById('dealer-list'); 1120 const $pageInfo = document.getElementById('dealer-page-info'); 1121 const $prev = document.getElementById('dealer-prev'); 1122 const $next = document.getElementById('dealer-next'); 1123 const $reset = document.getElementById('dealer-reset'); 1124 const $status = document.getElementById('dealer-status'); 1125 const $mapExternal = document.getElementById('dealer-map-external'); 1126 1127 let dealerMap = null; 1128 let dealerCluster = null; 1129 let dealerMarkers = new Map(); 1130 let czechRegionLayer = null; 1131 let czechRegionLoadStarted = false; 1132 1133 const CZECH_REGION_BOUNDARY_PATHS = [ 1134 'kraje.json', 1135 'data/kraje.json', 1136 'https://raw.githubusercontent.com/siwekm/czech-geojson/master/kraje.json' 1137 ]; 1138 1139 1140 const CZECH_REGIONAL_HUBS = [ 1141 { name: 'Praha a StÅednà Äechy', point: [14.4378, 50.0755] }, 1142 { name: 'Äeské BudÄjovice / Jižnà Äechy', point: [14.4743, 48.9745] }, 1143 { name: 'PlzeÅ / Západnà Äechy', point: [13.3776, 49.7384] }, 1144 { name: 'Karlovy Vary / Chebsko', point: [12.8712, 50.2319] }, 1145 { name: 'Ãstà nad Labem / Severozápad', point: [14.0323, 50.6607] }, 1146 { name: 'Liberec / Severnà Äechy', point: [15.0543, 50.7663] }, 1147 { name: 'Hradec Králové / Východnà Äechy', point: [15.8328, 50.2092] }, 1148 { name: 'Pardubice / Východnà Äechy', point: [15.7792, 50.0343] }, 1149 { name: 'Jihlava / VysoÄina', point: [15.5903, 49.3961] }, 1150 { name: 'Brno / Jižnà Morava', point: [16.6068, 49.1951] }, 1151 { name: 'ZlÃn / Východnà Morava', point: [17.6660, 49.2244] }, 1152 { name: 'Olomouc / StÅednà Morava', point: [17.2518, 49.5938] }, 1153 { name: 'Ostrava / Severnà Morava', point: [18.2625, 49.8209] } 1154 ]; 1155 1156 const CITY_POINTS_FOR_REGION = { 1157 'Praha': [14.4378, 50.0755], 'Brno': [16.6068, 49.1951], 'Äeské BudÄjovice': [14.4743, 48.9745], 1158 'PlzeÅ': [13.3776, 49.7384], 'Ostrava': [18.2625, 49.8209], 'Olomouc': [17.2518, 49.5938], 1159 'ZlÃn': [17.6660, 49.2244], 'Liberec': [15.0543, 50.7663], 'Pardubice': [15.7792, 50.0343], 1160 'Hradec Králové': [15.8328, 50.2092], 'Jihlava': [15.5903, 49.3961], 'Karlovy Vary': [12.8712, 50.2319], 1161 'Ãstà Nad Labem': [14.0323, 50.6607], 'Tábor': [14.6578, 49.4144], 'TÅebÃÄ': [15.8817, 49.2149], 1162 'ProstÄjov': [17.1118, 49.4719], 'Rokycany': [13.5946, 49.7427], 'Jablonec Nad Nisou': [15.1681, 50.7243], 1163 'Mladá Boleslav': [14.9032, 50.4114], 'Strakonice': [13.9024, 49.2615], 'Prachatice': [13.9976, 49.0129], 1164 'Chomutov': [13.4178, 50.4605], 'Studénka': [18.0787, 49.7234], 'VlaÅ¡im': [14.8988, 49.7063], 1165 'Znojmo': [16.0488, 48.8555], 'SuÅ¡ice': [13.5202, 49.2311], 'IvanÄice': [16.3775, 49.1014], 1166 'Kyjov': [17.1225, 49.0102], 'Äeský Krumlov': [14.3152, 48.8127], 'PÃsek': [14.1475, 49.3088], 1167 'Klatovy': [13.2951, 49.3955], 'HodonÃn': [17.1324, 48.8489], 'BÅeclav': [16.8820, 48.7589], 1168 'KolÃn': [15.2006, 50.0281], 'KadaÅ': [13.2713, 50.3833], 'ŽÄár Nad Sázavou': [15.9392, 49.5626], 1169 'LitomÄÅice': [14.1318, 50.5335], 'Opava': [17.9026, 49.9387], 'Karviná': [18.5417, 49.8540], 1170 'PÅerov': [17.4509, 49.4551], 'Uherské HradiÅ¡tÄ': [17.4597, 49.0698], 'VrchlabÃ': [15.6094, 50.6269] 1171 }; 1172 1173 function normalizeCountryForRegion(value) { 1174 const raw = String(value || '').trim(); 1175 const lower = raw.toLowerCase(); 1176 const aliases = { 1177 'cz': 'Czechia', 1178 'Är': 'Czechia', 1179 'czechia': 'Czechia', 1180 'czech republic': 'Czechia' 1181 }; 1182 return aliases[lower] || raw; 1183 } 1184 1185 function normalizeCityForRegion(value) { 1186 let city = String(value || '').trim().replace(/\s+/g, ' '); 1187 if (!city) return ''; 1188 city = city.replace(/\s*\(.*?\)\s*/g, ' '); 1189 city = city.replace(/\s*-\s*.*$/g, ''); 1190 city = city.trim().replace(/\s+/g, ' '); 1191 const lower = city.toLowerCase(); 1192 if (lower.startsWith('praha')) return 'Praha'; 1193 if (lower.startsWith('brno')) return 'Brno'; 1194 if (lower.startsWith('plzeÅ') || lower.startsWith('plzen')) return 'PlzeÅ'; 1195 if (lower.startsWith('ostrava')) return 'Ostrava'; 1196 if (lower.startsWith('hradec králové') || lower.startsWith('hradec kralove')) return 'Hradec Králové'; 1197 if (lower.startsWith('Äeské budÄjovice') || lower.startsWith('ceske budejovice')) return 'Äeské BudÄ
1197jovice'; 1198 if (lower.startsWith('znojmo')) return 'Znojmo'; 1199 if (lower.startsWith('bÅeclav') || lower.startsWith('breclav')) return 'BÅeclav'; 1200 if (lower.startsWith('ústà nad labem') || lower.startsWith('usti nad labem')) return 'Ãstà Nad Labem'; 1201 return city.toLowerCase().split(' ').filter(Boolean).map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' '); 1202 } 1203 1204 function distanceSquaredForRegion(pointA, pointB) { 1205 const lonDiff = pointA[0] - pointB[0]; 1206 const latDiff = pointA[1] - pointB[1]; 1207 return lonDiff * lonDiff + latDiff * latDiff; 1208 } 1209 1210 function nearestRegionalHubForIndex(point) { 1211 if (!point) return null; 1212 let bestHub = null; 1213 let bestDistance = Infinity; 1214 CZECH_REGIONAL_HUBS.forEach(hub => { 1215 const distance = distanceSquaredForRegion(point, hub.point); 1216 if (distance < bestDistance) { 1217 bestDistance = distance; 1218 bestHub = hub; 1219 } 1220 }); 1221 return bestHub; 1222 } 1223 1224 function itemRegionalHubName(item) { 1225 if (normalizeCountryForRegion(item.country) !== 'Czechia') return ''; 1226 const city = normalizeCityForRegion(item.city); 1227 const point = hasCoordinates(item) ? [Number(item.lon), Number(item.lat)] : CITY_POINTS_FOR_REGION[city]; 1228 const hub = nearestRegionalHubForIndex(point); 1229 return hub ? hub.name : ''; 1230 } 1231 1232 function normalizeWebsite(url) { 1233 if (!url) return ''; 1234 const value = String(url).trim(); 1235 if (!value) return ''; 1236 if (/^https?:\/\//i.test(value)) return value; 1237 return 'https://' + value.replace(/^\/+/, ''); 1238 } 1239 1240 function normalizePhone(phone) { 1241 if (!phone) return ''; 1242 return String(phone).replace(/[^\d+]/g, ''); 1243 } 1244 1245 function escapeHtml(value) { 1246 return String(value || '') 1247 .replace(/&/g, '&') 1248 .replace(/</g, '<') 1249 .replace(/>/g, '>') 1250 .replace(/"/g, '"') 1251 .replace(/'/g, '''); 1252 } 1253 1254 1255 function normalizeSearchText(value) { 1256 return String(value || '') 1257 .toLowerCase() 1258 .normalize('NFD') 1259 .replace(/[\u0300-\u036f]/g, '') 1260 .replace(/\s+/g, ' ') 1261 .trim(); 1262 } 1263 1264 function isCzechCountry(value) { 1265 const normalized = normalizeSearchText(value); 1266 return ['ceska republika', 'czech republic', 'czechia', 'cz', 'cr'].includes(normalized); 1267 } 1268 1269 function selectedCountryIsCzechRepublic() { 1270 return isCzechCountry($country.value); 1271 } 1272 1273 function updateCzechRegionFilterState() { 1274 const enabled = selectedCountryIsCzechRepublic(); 1275 1276 $scope.disabled = !enabled; 1277 $scope.classList.toggle('bg-zinc-100', !enabled); 1278 $scope.classList.toggle('text-zinc-400', !enabled); 1279 $scope.classList.toggle('cursor-not-allowed', !enabled); 1280 $scope.classList.toggle('bg-white', enabled); 1281 $scope.classList.toggle('text-zinc-900', enabled); 1282 1283 if (!enabled) { 1284 $scope.value = ''; 1285 } 1286 } 1287 1288 function itemSearchHaystack(item) { 1289 return normalizeSearchText([ 1290 item.name, 1291 item.city, 1292 item.country, 1293 item.street, 1294 item.person, 1295 item.email, 1296 item.web, 1297 item.addressLabel, 1298 item.geoQuery, 1299 item.geocodeDisplayName, 1300 item.isPickupPoint ? 'výdejnà placeso vydejni misto pickup point' : '' 1301 ].filter(Boolean).join(' ')); 1302 } 1303 1304 function buildExternalMapUrl(item) { 1305 if (item && hasCoordinates(item)) { 1306 return `https://www.google.com/maps?q=${encodeURIComponent(Number(item.lat) + ',' + Number(item.lon))}`; 1307 } 1308 1309 return `https://www.google.com/maps?q=${encodeURIComponent(buildMapQuery(item))}`; 1310 } 1311 1312 function getBadge(itemOrType, scope) { 1313 const item = typeof itemOrType === 'object' && itemOrType ? itemOrType : null; 1314 const labels = []; 1315 1316 if (item) { 1317 labels.push('Dealer'); 1318 if (item.isServicePoint || item.type === 'service') labels.push('Service point'); 1319 if (item.isPickupPoint) labels.push('Pickup point'); 1320 1321 const scopeLabel = item.scope === 'foreign' ? 'abroad' : 'Czechia'; 1322 return `${labels.join(' ⢠')} ⢠${scopeLabel}`; 1323 } 1324 1325 const typeLabel = itemOrType === 'service' 1326 ? 'Service point' 1327 : (itemOrType === 'pickup' ? 'Pickup point' : 'Dealer'); 1328 const scopeLabel = scope === 'foreign' ? 'abroad' : 'Czechia'; 1329 return `${typeLabel} ⢠${scopeLabel}`; 1330 } 1331 1332 function buildAddress(item) { 1333 return [item.street, item.city, item.country].filter(
1333Boolean).join(', '); 1334 } 1335 1336 function buildMapQuery(item) { 1337 if (!item) return 'Czechia'; 1338 const parts = [item.name, item.street, item.city, item.country].filter(Boolean); 1339 return parts.join(', '); 1340 } 1341 1342 function hasCoordinates(item) { 1343 return Number.isFinite(Number(item.lat)) && Number.isFinite(Number(item.lon)); 1344 } 1345 1346 1347 function clusterTier(markers) { 1348 const tiers = markers 1349 .map(marker => marker.options && marker.options.leaderfoxTier) 1350 .filter(Boolean); 1351 1352 if (tiers.includes('gold')) return 'gold'; 1353 if (tiers.includes('silver')) return 'silver'; 1354 if (tiers.includes('bronze')) return 'bronze'; 1355 return 'standard'; 1356 } 1357 1358 function clusterIconCreate(cluster) { 1359 const markers = cluster.getAllChildMarkers(); 1360 const tier = clusterTier(markers); 1361 const count = cluster.getChildCount(); 1362 const sizeClass = count < 10 ? 'marker-cluster-small' : (count < 100 ? 'marker-cluster-medium' : 'marker-cluster-large'); 1363 const tierClass = tier !== 'standard' ? ` cluster-${tier}` : ''; 1364 1365 return L.divIcon({ 1366 html: `<div><span>${count}</span></div>`, 1367 className: `marker-cluster ${sizeClass}${tierClass}`, 1368 iconSize: L.point(40, 40) 1369 }); 1370 } 1371 1372 function regionNameFromFeature(feature) { 1373 const props = feature && feature.properties ? feature.properties : {}; 1374 return props.NAZEV || 1375 props.NAZEV_KRAJ || 1376 props.NAZ_CZNUTS3 || 1377 props.name || 1378 props.NAME || 1379 props.nazev || 1380 props.KOD_NUTS3 || 1381 ''; 1382 } 1383 1384 function styleCzechRegionBoundary() { 1385 return { 1386 color: '#a16207', 1387 weight: 2.4, 1388 opacity: 0.95, 1389 fillColor: '#a16207', 1390 fillOpacity: 0.035, 1391 dashArray: '', 1392 interactive: true 1393 }; 1394 } 1395 1396 async function loadCzechRegionBoundaries() { 1397 if (!dealerMap || czechRegionLoadStarted) return; 1398 czechRegionLoadStarted = true; 1399 1400 for (const path of CZECH_REGION_BOUNDARY_PATHS) { 1401 try { 1402 const response = await fetch(path + (path.includes('?') ? '&' : '?') + 'v=' + Date.now()); 1403 1404 if (!response.ok) continue; 1405 1406 const geojson = await response.json(); 1407 1408 if (dealerMap.getPane('czech-regions-pane') === undefined) { 1409 dealerMap.createPane('czech-regions-pane'); 1410 dealerMap.getPane('czech-regions-pane').style.zIndex = 420; 1411 dealerMap.getPane('czech-regions-pane').style.pointerEvents = 'auto'; 1412 } 1413 1414 czechRegionLayer = L.geoJSON(geojson, { 1415 pane: 'czech-regions-pane', 1416 style: styleCzechRegionBoundary, 1417 onEachFeature: (feature, layer) => { 1418 const regionName = regionNameFromFeature(feature); 1419 1420 if (regionName) { 1421 layer.bindTooltip(regionName, { 1422 sticky: true, 1423 direction: 'top', 1424 className: 'czech-region-tooltip' 1425 }); 1426 } 1427 1428 layer.on('mouseover', () => { 1429 layer.setStyle({ 1430 color: '#854d0e', 1431 weight: 3.6, 1432 opacity: 1, 1433 fillOpacity: 0.10 1434 }); 1435 }); 1436 1437 layer.on('mouseout', () => { 1438 czechRegionLayer.resetStyle(layer); 1439 }); 1440 } 1441 }).addTo(dealerMap); 1442 1443 return; 1444 } catch (e) {} 1445 } 1446
1447 console.warn('Hranice krajů ÄR se nepodaÅilo naÄÃst.'); 1448 } 1449 1450 function initDealerMap() { 1451 if (dealerMap) return; 1452 1453 dealerMap = L.map('dealer-map-frame', { 1454 scrollWheelZoom: false 1455 }).setView([49.8, 15.5], 7); 1456 1457 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 1458 maxZoom: 19, 1459 attribution: '© OpenStreetMap' 1460 }).addTo(dealerMap); 1461 1462 dealerCluster = L.markerClusterGroup({ 1463 showCoverageOnHover: false, 1464 spiderfyOnMaxZoom: true, 1465 maxClusterRadius: 48, 1466 iconCreateFunction: clusterIconCreate 1467 }); 1468 1469 dealerMap.addLayer(dealerCluster); 1470 loadCzechRegionBoundaries(); 1471 } 1472 1473 1474 function getPremiumLabel(item) { 1475 if (!item || !item.isPremiumDealer) return ''; 1476 if (item.leaderfoxTierLabel) return item.leaderfoxTierLabel; 1477 if (item.leaderfoxTier === 'gold') return 'Zlatý partner'; 1478 if (item.leaderfoxTier === 'silver') return 'StÅÃbrný partner'; 1479 if (item.leaderfoxTier === 'bronze') return 'Bronzový partner'; 1480 return 'Prémiový partner'; 1481 } 1482 1483 function getPremiumBadgeHtml(item) { 1484 const label = getPremiumLabel(item); 1485 if (!label) return ''; 1486 const tier = item.leaderfoxTier || 'premium'; 1487 return `<span class="premium-badge ${escapeHtml(tier)}">${escapeHtml(label)}</span>`; 1488 } 1489 1490 function premiumFilterMatches(item, value) { 1491 if (!value) return true; 1492 if (value === 'premium') return !!item.isPremiumDealer; 1493 return item.leaderfoxTier === value; 1494 } 1495 1496 function sortDealerItems(items) { 1497 return [...items].sort((a, b) => { 1498 const ar = Number.isFinite(Number(a.leaderfoxRank)) ? Number(a.leaderfoxRank) : 999999; 1499 const br = Number.isFinite(Number(b.leaderfoxRank)) ? Number(b.leaderfoxRank) : 999999; 1500 if (ar !== br) return ar - br; 1501 return String(a.name || '').localeCompare(String(b.name || ''), 'cs'); 1502 }); 1503 } 1504 1505 function markerIcon(item) { 1506 const iconSymbol = (item.isServicePoint || item.type === 'service') ? 'ð§' : (item.isPickupPoint ? 'ð¦' : 'ð²'); 1507 let className = (item.isServicePoint || item.type === 'service') ? 'service' : (item.isPickupPoint ? 'pickup' : 'dealer'); 1508 1509 if (item.isPremiumDealer && item.leaderfoxTier === 'gold') { 1510 className = 'gold'; 1511 } else if (item.isPremiumDealer && item.leaderfoxTier === 'silver') { 1512 className = 'silver'; 1513 } else if (item.isPremiumDealer && item.leaderfoxTier === 'bronze') { 1514 className = 'bronze'; 1515 } 1516 1517 return L.divIcon({ 1518 className: 'dealer-type-icon-wrap', 1519 html: `<span class="dealer-type-icon ${escapeHtml(className)}" aria-hidden="true">${iconSymbol}</span>`, 1520 iconSize: [38, 38], 1521 iconAnchor: [19, 19], 1522 popupAnchor: [0, -22] 1523 }); 1524 } 1525 1526 function popupHtml(item) { 1527 const address = buildAddress(item); 1528 const website = normalizeWebsite(item.web); 1529 const phone = normalizePhone(item.phone); 1530 1531 return ` 1532 <div class="dealer-map-popup"> 1533 <div class="dealer-map-popup-head"> 1534 <div class="dealer-map-popup-title">${escapeHtml(item.name || '')}</div> 1535 <div class="dealer-map-popup-strip" style="--partner-strip-color:${partnerStripColor(item)}"></div> 1536 </div> 1537 ${address ? `<div class="dealer-map-popup-text">${escapeHtml(address)}</div>` : ''} 1538 ${item.phone ? `<div class="dealer-map-popup-text">Tel.: <a href="tel:${escapeHtml(phone)}">${escapeHtml(item.phone)}</a></div>` : ''} 1539 ${item.email ? `<div class="dealer-map-popup-text">E-mail: <a href="mailto:${escapeHtml(item.email)}">${escapeHtml(item.email)}</a></div>` : ''} 1540 ${website ? `<div class="dealer-map-popup-text">Web: <a href="${escapeHtml(website)}" target="_blank" rel="noopener">${escapeHtml(website.replace(/^https?:\/\//, ''))}</a></div>` : ''} 1541 <div style="display:flex; flex-direction:column; gap:6px; margin-top:8px;"> 1542 <a href="${escapeHtml(buildExternalMapUrl(item))}" target="_blank" rel="noopener" class="dealer-map-popup-link">Open in maps</a> 1543 <a href="#dealer-list" class="dealer-map-popup-link" data-popup-id="${escapeHtml(item.id)}">Show in list</a> 1544 </div> 1545 </div> 1546 `; 1547 } 1548 1549 function renderDealerMap(items, focusItem = null, labelText = '', globalDefault = false) { 1550 initDealerMap(); 1551 1552 dealerCluster.clearLayers(); 1553 dealerMarkers.clear(); 1554 1555 const itemsWithCoordinates = items.filter(hasCoordinates); 1556
1557 itemsWithCoordinates.forEach(item => { 1558 const marker = L.marker([Number(item.lat), Number(item.lon)], { 1559 icon: markerIcon(item), 1560 leaderfoxTier: item.isPremiumDealer ? item.leaderfoxTier : '' 1561 }); 1562 marker.bindPopup(popupHtml(item)); 1563 marker.on('click', () => { 1564 filteredData = [item]; 1565 currentPage = 1; 1566 $search.value = item.name || ''; 1567 searchFilledFromMarker = true; 1568 $type.value = ''; 1569 $scope.value = ''; 1570 $country.value = ''; 1571 1572 renderCurrentPage(); 1573 renderDealerMap([item], item, `The map and list show the company: ${item.name || ''}`); 1574 setActiveCard(item.id); 1575 1576 dealerMap.setView([Number(item.lat), Number(item.lon)], Math.max(dealerMap.getZoom(), 16)); 1577 $mapExternal.href = buildExternalMapUrl(item); 1578 1579 const markerAfterRender = dealerMarkers.get(item.id); 1580 if (markerAfterRender) { 1581 markerAfterRender.openPopup(); 1582 } 1583 1584 document.getElementById('dealer-map').scrollIntoView({ behavior: 'smooth', block: 'start' }); 1585 }); 1586 marker.on('dblclick', () => { 1587 const itemIndex = filteredData.findIndex(found => found.id === item.id); 1588 1589 if (itemIndex >= 0) { 1590 currentPage = Math.floor(itemIndex / PAGE_SIZE) + 1; 1591 renderCurrentPage(); 1592 1593 setTimeout(() => {
1594 setActiveCard(item.id); 1595 const card = document.querySelector(`.dealer-card[data-id="${CSS.escape(item.id)}"]`); 1596 1597 if (card) { 1598 card.scrollIntoView({ behavior: 'smooth', block: 'center' }); 1599 card.classList.add('ring-4','ring-yellow-400'); 1600 setTimeout(() => card.classList.remove('ring-4','ring-yellow-400'), 2000); 1601 } 1602 }, 120); 1603 } 1604 }); 1605 dealerMarkers.set(item.id, marker); 1606 dealerCluster.addLayer(marker); 1607 }); 1608 1609 if (focusItem && hasCoordinates(focusItem)) { 1610 const marker = dealerMarkers.get(focusItem.id); 1611 dealerMap.setView([Number(focusItem.lat), Number(focusItem.lon)], 15); 1612 if (marker) marker.openPopup(); 1613 $mapExternal.href = buildExternalMapUrl(focusItem); 1614 } else if (itemsWithCoordinates.length) { 1615 if (globalDefault) { 1616 dealerMap.setView([20, 0], 2); 1617 $mapExternal.href = 'https://www.google.com/maps?q=world'; 1618 } else { 1619 const bounds = L.latLngBounds(itemsWithCoordinates.map(item => [Number(item.lat), Number(item.lon)])); 1620 dealerMap.fitBounds(bounds, { padding: [36, 36], maxZoom: 13 }); 1621 const first = itemsWithCoordinates[0]; 1622 $mapExternal.href = buildExternalMapUrl(first); 1623 } 1624 } else { 1625 dealerMap.setView([20, 0], 2); 1626 $mapExternal.href = 'https://www.google.com/maps?q=world'; 1627 } 1628 1629 const withoutCoordinates = items.length - itemsWithCoordinates.length; 1630 if (labelText) { 1631 $status.textContent = labelText; 1632 } else if (itemsWithCoordinates.length) { 1633 $status.textContent = `The map shows ${itemsWithCoordinates.length} places with coordinates.` + (withoutCoordinates ? ` ${withoutCoordinates} places do not yet have coordinates.` : ''); 1634 } else { 1635 $status.textContent = 'The current selection does not yet contain any records with coordinates.'; 1636 } 1637 } 1638 1639 function fillCountryFilter(data) { 1640 const countries = [...new Set(data.map(item => item.country).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'cs')); 1641 $country.innerHTML = '<option value="">All countries</option>' + countries.map(country => `<option value="${escapeHtml(country)}">${escapeHtml(country)}</option>`).join(''); 1642 } 1643 1644 function partnerStripColor(item) { 1645 if (item && item.isPremiumDealer && item.leaderfoxTier === 'gold') return '#facc15'; 1646 if (item && item.isPremiumDealer && item.leaderfoxTier === 'silver') return '#d4d4d8'; 1647 if (item && item.isPremiumDealer && item.leaderfoxTier === 'bronze') return '#b45309'; 1648 return '#86efac'; 1649 } 1650 1651 function pickupPointBadgeHtml(item) { 1652 return ''; 1653 } 1654 1655 function itemCardHtml(item) { 1656 const address = buildAddress(item); 1657 const website = normalizeWebsite(item.web); 1658 const phone = normalizePhone(item.phone); 1659 1660 return ` 1661 <div class="dealer-card w-full rounded-3xl border border-zinc-200 bg-white p-5 pl-7 hover:border-yellow-400 hover:bg-yellow-50/40 transition ${item.isPremiumDealer ? 'dealer-card-premium' : ''}" data-id="${escapeHtml(item.id)}" style="--partner-strip-color:${partnerStripColor(item)}"> 1662 <div class="flex flex-wrap items-center gap-2 mb-2"><span class="text-[11px] uppercase tracking-[0.18em] font-black text-yellow-600">${escapeHtml(getBadge(item))}</span>${pickupPointBadgeHtml(item)}</div> 1663 <div class="text-lg font-black text-zinc-900 leading-tight mb-2">${escapeHtml(item.name || '')}</div> 1664 ${address ? `<div class="text-sm text-zinc-600 mb-3">${escapeHtml(address)}</div>` : ''} 1665 1666 <div class="space-y-1 text-sm text-zinc-500 mb-4"> 1667 ${item.person ? `<div>${escapeHtml(item.person)}</div>` : ''} 1668 ${item.phone ? `<div>Tel.: <a class="dealer-link" href="tel:${escapeHtml(phone)}">${escapeHtml(item.phone)}</a></div>` : ''} 1669 ${item.email ? `<div>E-mail: <a class="dealer-link" href="mailto:${escapeHtml(item.email)}">${escapeHtml(item.email)}</a></div>` : ''} 1670 ${website ? `<div>Web: <a class="dealer-link" href="${escapeHtml(website)}" target="_blank" rel="noopener">${escapeHtml(website.replace(/^https?:\/\//, ''))}</a></div>` : ''} 1671 </div> 1672 1673 <button type="button" class="dealer-focus inline-flex items-center justify-center rounded-xl border border-zinc-900 px-4 py-2 text-sm font-black text-zinc-900 hover:bg-zinc-900 hover:text-white transition" data-id="${escapeHtml(item.id)}"> 1674 Show on map 1675 </button> 1676 </div> 1677 `; 1678 } 1679 1680 function clearActiveCards() { 1681 document.querySelectorAll('.dealer-card').forEach(card => card.classList.remove('dealer-card-active')); 1682 } 1683 1684 function setActiveCard(itemId) { 1685 activeCardId = itemId; 1686 clearActiveCards(); 1687 const card = document.querySelector(`.dealer-card[data-id="${CSS.escape(itemId)}"]`); 1688 if (card) { 1689 card.classList.add('dealer-card-active'); 1690 } 1691 } 1692 1693 function getPageItems() { 1694 const start = (currentPage - 1) * PAGE_SIZE; 1695 return filteredData.slice(start, start + PAGE_SIZE); 1696 } 1697 1698 function showDealerOnMap(itemId, scrollToMap = false) { 1699 const item = dealerData.find(x => x.id === itemId); 1700 if (!item) return; 1701 1702 renderDealerMap(filteredData, item, hasCoordinates(item) 1703 ? `The map shows the company: ${item.name || ''}` 1704 : `Firma ${item.name || ''} does not yet have lat/lon coordinates in the JSON.`); 1705
1706 setActiveCard(itemId); 1707 1708 if (scrollToMap) { 1709 document.getElementById('dealer-map').scrollIntoView({ behavior: 'smooth', block: 'start' }); 1710 } 1711 } 1712 1713 function applyFilters() { 1714 const q = normalizeSearchText(($search.value || '').trim()); 1715 const type = $type.value; 1716 const scope = $scope.value; 1717 const country = $country.value; 1718 const selectedRegion = scope && scope.startsWith('region:') ? scope.slice(7) : ''; 1719 const selectedRegionNormalized = normalizeSearchText(selectedRegion); 1720 1721 filteredData = dealerData.filter(item => { 1722 const haystack = itemSearchHaystack(item); 1723 1724 if (q && !haystack.includes(q)) return false; 1725 if (type === 'pickup' && !item.isPickupPoint) return false; 1726 if (type === 'service' && !(item.isServicePoint || item.type === 'service')) return false; 1727 if (type === 'dealer' && item.type !== 'dealer') return false; 1728 if (country && item.country !== country) return false; 1729 1730 if (selectedRegion && selectedCountryIsCzechRepublic()) { 1731 if (!isCzechCountry(item.country)) return false; 1732 if (!haystack.includes(selectedRegionNormalized)) return false; 1733 } 1734 1735 1736 return true; 1737 }); 1738 1739 filteredData = sortDealerItems(filteredData); 1740 currentPage = 1; 1741 renderCurrentPage(); 1742 } 1743 1744 function renderCurrentPage() { 1745 activeCardId = null; 1746 1747 const total = filteredData.length; 1748 const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); 1749 1750 if (currentPage > totalPages) currentPage = totalPages; 1751 1752 const pageItems = getPageItems(); 1753 1754 $count.textContent = total ? `Found ${total} places` : 'No places found'; 1755 $pageInfo.textContent = `Page ${currentPage} z ${totalPages}`; 1756 $prev.disabled = currentPage <= 1; 1757 $next.disabled = currentPage >= totalPages; 1758 1759 if (!pageItems.length) { 1760 $list.innerHTML = '<div class="md:col-span-2 xl:col-span-3 rounded-2xl border border-dashed border-zinc-300 p-5 text-sm text-zinc-500">No places were found for the selected filter.</div>'; 1761 renderDealerMap([], null, 'No places were found for the selected filter.'); 1762 return; 1763 } 1764 1765 $list.innerHTML = pageItems.map(item => itemCardHtml(item)).join(''); 1766 1767 document.querySelectorAll('.dealer-focus').forEach(button => { 1768 button.addEventListener('click', (event) => { 1769 event.preventDefault(); 1770 event.stopPropagation(); 1771 showDealerOnMap(button.dataset.id, true); 1772 }); 1773 }); 1774 1775 const firstItem = pageItems[0]; 1776 if (firstItem) { 1777 const hasActiveFilter = Boolean( 1778 $search.value.trim() || 1779 $type.value || 1780 $scope.value || 1781 $country.value 1782 ); 1783 1784 if (hasActiveFilter) { 1785 renderDealerMap(filteredData, null);
1786 setActiveCard(firstItem.id); 1787 } else { 1788 renderDealerMap(filteredData, null, 'The map is currently not filtered and shows the overall overview.', true); 1789 clearActiveCards(); 1790 } 1791 } 1792 } 1793 1794 async function loadDealerData() { 1795 const paths = [ 1796 'data/leaderfox-locations.json', 1797 'leaderfox-locations.json' 1798 ]; 1799 1800 for (const path of paths) { 1801 try { 1802 const response = await fetch(path + '?v=' + Date.now()); 1803 if (response.ok) { 1804 return await response.json(); 1805 } 1806 } catch (e) {} 1807 } 1808 1809 throw new Error('The partner data file was not found.'); 1810 } 1811 1812 async function initDealers() { 1813 try { 1814 dealerData = sortDealerItems(await loadDealerData()); 1815 fillCountryFilter(dealerData); 1816 updateCzechRegionFilterState(); 1817 filteredData = sortDealerItems(dealerData); 1818 1819 const params = new URLSearchParams(window.location.search); 1820 const partnerSearch = params.get('partnerSearch'); 1821 const partnerRegion = params.get('partnerRegion'); 1822 const partnerType = params.get('partnerType'); 1823 1824 if (partnerType && $type) { 1825 $type.value = partnerType; 1826 } 1827 1828 if (partnerRegion) { 1829 const czechCountryOption = Array.from($country.options).find(option => isCzechCountry(option.value)); 1830 1831 if (czechCountryOption) { 1832 $country.value = czechCountryOption.value; 1833 } 1834 1835 updateCzechRegionFilterState(); 1836 1837 const regionOption = Array.from($scope.options).find(option => option.value === 'region:' + partnerRegion); 1838 1839 if (regionOption && !$scope.disabled) { 1840 $scope.value = regionOption.value; 1841 applyFilters(); 1842 } else { 1843 $search.value = partnerRegion; 1844 filteredData = sortDealerItems(dealerData.filter(item => { 1845 if (itemRegionalHubName(item) !== partnerRegion) return false; 1846 if (partnerType === 'pickup' && !item.isPickupPoint) return false; 1847 if (partnerType === 'service' && !(item.isServicePoint || item.type === 'service')) return false; 1848 if (partnerType === 'dealer' && item.type !== 'dealer') return false; 1849 return true; 1850 })); 1851 currentPage = 1; 1852 renderCurrentPage(); 1853 } 1854 1855 setTimeout(() => { 1856 document.getElementById('partneri').scrollIntoView({ 1857 behavior: 'smooth', 1858 block: 'start' 1859 }); 1860 }, 250); 1861 } else if (partnerSearch) { 1862 $search.value = partnerSearch; 1863 applyFilters(); 1864 1865 setTimeout(() => { 1866 document.getElementById('partneri').scrollIntoView({ 1867 behavior: 'smooth', 1868 block: 'start' 1869 }); 1870 }, 250); 1871 } else { 1872 renderCurrentPage(); 1873 } 1874 } catch (e) { 1875 console.error(e); 1876 $count.textContent = 'Partner data could not be loaded.'; 1877 $status.textContent = ''; 1878 $list.innerHTML = '<div class="md:col-span-2 xl:col-span-3 rounded-2xl border border-dashed border-red-300 p-5 text-sm text-red-600">The leaderfox-locations.json file could not be loaded.</div>'; 1879 if (dealerMap) renderDealerMap([], null, ''); 1880 } 1881 } 1882 1883 document.addEventListener('click', (event) => { 1884 const link = event.target.closest('[data-popup-id]'); 1885 if (!link) return; 1886 1887 event.preventDefault(); 1888 const itemId = link.getAttribute('data-popup-id'); 1889 const itemIndex = filteredData.findIndex(item => item.id === itemId); 1890 1891 if (itemIndex >= 0) { 1892 const item = filteredData[itemIndex]; 1893 filteredData = [item]; 1894 currentPage = 1; 1895 $search.value = item.name || ''; 1896 renderCurrentPage(); 1897 1898 setTimeout(() => {
1899 setActiveCard(itemId); 1900 const card = document.querySelector(`.dealer-card[data-id="${CSS.escape(itemId)}"]`); 1901 if (card) { 1902 card.scrollIntoView({ behavior: 'smooth', block: 'center' }); 1903 } 1904 }, 80); 1905 } 1906 }); 1907 1908 $search.addEventListener('input', () => { 1909 searchFilledFromMarker = false; 1910 applyFilters(); 1911 }); 1912 function applyFiltersAfterSelectChange() { 1913 if ($search.value) { 1914 $search.value = ''; 1915 } 1916 1917 searchFilledFromMarker = false; 1918 updateCzechRegionFilterState(); 1919 applyFilters(); 1920 } 1921 1922 $type.addEventListener('change', applyFiltersAfterSelectChange); 1923 $scope.addEventListener('change', applyFiltersAfterSelectChange); 1924 $country.addEventListener('change', applyFiltersAfterSelectChange); 1925 1926 $prev.addEventListener('click', () => { 1927 if (currentPage > 1) { 1928 currentPage--; 1929 renderCurrentPage(); 1930 } 1931 }); 1932 1933 $next.addEventListener('click', () => { 1934 const totalPages = Math.max(1, Math.ceil(filteredData.length / PAGE_SIZE)); 1935 if (currentPage < totalPages) { 1936 currentPage++; 1937 renderCurrentPage(); 1938 } 1939 }); 1940 1941 $reset.addEventListener('click', () => { 1942 $search.value = ''; 1943 searchFilledFromMarker = false;
1944 $type.value = ''; 1945 $scope.value = ''; 1946 $country.value = ''; 1947 updateCzechRegionFilterState(); 1948 filteredData = sortDealerItems(dealerData); 1949 currentPage = 1; 1950 renderCurrentPage(); 1951 }); 1952 1953 1954 const GALLERY_SETS = { 1955 hero: { 1956 title: 'Hero', 1957 path: 'images/hero/', 1958 prefix: 'HERO', 1959 count: 9, 1960 extension: '.jpg' 1961 }, 1962 alps: { 1963 title: 'Alps', 1964 path: 'images/alps/', 1965 prefix: 'ALPS', 1966 count: 149, 1967 extension: '.jpg' 1968 } 1969 }; 1970 1971 let currentGallerySet = 'hero'; 1972 let currentGalleryVisible = 9; 1973 let currentGalleryImages = []; 1974 let currentGalleryIndex = 0; 1975 1976 function galleryFileName(config, index) { 1977 return `${config.prefix}${String(index).padStart(3, '0')}${config.extension}`; 1978 } 1979 1980 function buildGalleryImages(setName) { 1981 const config = GALLERY_SETS[setName]; 1982 return Array.from({ length: config.count }, (_, index) => { 1983 const number = index + 1; 1984 const fileName = galleryFileName(config, number); 1985 return { 1986 set: setName, 1987 title: `${config.title} ${String(number).padStart(3, '0')}`, 1988 src: config.path + fileName 1989 }; 1990 }); 1991 } 1992 1993 function galleryThumbHtml(image, index) { 1994 return ` 1995 <button type="button" class="gallery-thumb card-lift" data-gallery-index="${index}" aria-label="Open photo ${escapeHtml(image.title)}"> 1996 <img src="${escapeHtml(image.src)}" alt="${escapeHtml(image.title)}" loading="lazy" onerror="this.closest('.gallery-thumb').remove();"> 1997 </button> 1998 `; 1999 } 2000 2001 function renderGallery() { 2002 const $galleryGrid = document.getElementById('gallery-grid'); 2003 const $loadMore = document.getElementById('gallery-load-more'); 2004 2005 if (!$galleryGrid || !$loadMore) return; 2006 2007 currentGalleryImages = buildGalleryImages(currentGallerySet); 2008 const visibleImages = currentGalleryImages.slice(0, currentGalleryVisible); 2009 2010 $galleryGrid.innerHTML = visibleImages.map(galleryThumbHtml).join(''); 2011 $loadMore.classList.toggle('hidden', currentGalleryVisible >= currentGalleryImages.length); 2012 2013 document.querySelectorAll('.gallery-thumb').forEach(button => { 2014 button.addEventListener('click', () => { 2015 openGalleryLightbox(Number(button.dataset.galleryIndex || 0)); 2016 }); 2017 }); 2018 } 2019 2020 function setGallerySet(setName) { 2021 if (!GALLERY_SETS[setName]) return; 2022 2023 currentGallerySet = setName; 2024 currentGalleryVisible = setName === 'hero' ? 9 : 12; 2025 2026 document.querySelectorAll('.gallery-tab').forEach(tab => { 2027 const isActive = tab.dataset.gallerySet === setName; 2028 tab.classList.toggle('is-active', isActive); 2029 }); 2030 2031 renderGallery(); 2032 } 2033 2034 function openGalleryLightbox(index) { 2035 const $lightbox = document.getElementById('gallery-lightbox'); 2036 const $image = document.getElementById('gallery-lightbox-image'); 2037 const $caption = document.getElementById('gallery-lightbox-caption'); 2038 2039 if (!$lightbox || !$image || !$caption || !currentGalleryImages.length) return; 2040 2041 currentGalleryIndex = Math.max(0, Math.min(index, currentGalleryImages.length - 1)); 2042 const image = currentGalleryImages[currentGalleryIndex]; 2043 2044 $image.src = image.src; 2045 $image.alt = image.title; 2046 $caption.textContent = `${image.title} / ${currentGalleryIndex + 1} z ${currentGalleryImages.length}`; 2047 $lightbox.classList.add('is-open'); 2048 $lightbox.setAttribute('aria-hidden', 'false'); 2049 document.body.style.overflow = 'hidden'; 2050 } 2051 2052 function closeGalleryLightbox() { 2053 const $lightbox = document.getElementById('gallery-lightbox'); 2054 const $image = document.getElementById('gallery-lightbox-image'); 2055 2056 if (!$lightbox || !$image) return; 2057 2058 $lightbox.classList.remove('is-open'); 2059 $lightbox.setAttribute('aria-hidden', 'true'); 2060 $image.src = ''; 2061 document.body.style.overflow = ''; 2062 } 2063 2064 function moveGalleryLightbox(step) { 2065 if (!currentGalleryImages.length) return; 2066 const nextIndex = (currentGalleryIndex + step + currentGalleryImages.length) % currentGalleryImages.length; 2067 openGalleryLightbox(nextIndex); 2068 } 2069 2070 function initGallery() { 2071 const $loadMore = document.getElementById('gallery-load-more'); 2072 const $close = document.getElementById('gallery-lightbox-close'); 2073 const $prev = document.getElementById('gallery-lightbox-prev'); 2074 const $next = document.getElementById('gallery-lightbox-next'); 2075 const $lightbox = document.getElementById('gallery-lightbox'); 2076 2077 if (!document.getElementById('gallery-grid')) return; 2078 2079 document.querySelectorAll('.gallery-tab').forEach(tab => { 2080 tab.addEventListener('click', () => setGallerySet(tab.dataset.gallerySet)); 2081 }); 2082 2083 if ($loadMore) { 2084 $loadMore.addEventListener('click', () => { 2085 currentGalleryVisible += 12; 2086 renderGallery(); 2087 }); 2088 } 2089 2090 if ($close) $close.addEventListener('click', closeGalleryLightbox); 2091 if ($prev) $prev.addEventListener('click', () => moveGalleryLightbox(-1)); 2092 if ($next) $next.addEventListener('click', () => moveGalleryLightbox(1)); 2093 2094 if ($lightbox) { 2095 $lightbox.addEventListener('click', (event) => { 2096 if (event.target === $lightbox) closeGalleryLightbox(); 2097 }); 2098 } 2099 2100 document.addEventListener('keydown', (event) => { 2101 const $box = document.getElementById('gallery-lightbox'); 2102 if (!$box || !$box.classList.contains('is-open')) return; 2103 2104 if (event.key === 'Escape') closeGalleryLightbox(); 2105 if (event.key === 'ArrowLeft') moveGalleryLightbox(-1); 2106 if (event.key === 'ArrowRight') moveGalleryLightbox(1); 2107 }); 2108 2109 setGallerySet('hero'); 2110 } 2111 2112 2113 function blogCardHtml(post) { 2114 const title = escapeHtml(post.title || ''); 2115 const text = escapeHtml(post.text || ''); 2116 const url = escapeHtml(post.url || 'https://e-shop.leaderfox.com/aktualita/'); 2117 const image = escapeHtml(post.image || ''); 2118 const date = escapeHtml(post.date || ''); 2119 2120 return ` 2121 <article class="card-lift rounded-3xl border border-zinc-200 bg-white overflow-hidden text-left"> 2122 ${image ? ` 2123 <a href="${url}" target="_blank" rel="noopener" class="block bg-zinc-100"> 2124 <img src="${image}" alt="${title}" class="w-full h-52 object-cover"> 2125 </a> 2126 ` : ` 2127 <a href="${url}" target="_blank" rel="noopener" class="block h-52 bg-zinc-900 relative overflow-hidden"> 2128 <div class="absolute inset-0 bg-gradient-to-br from-zinc-900 via-zinc-800 to-yellow-500/40"></div> 2129 <div class="absolute left-6 bottom-6 text-white text-[11px] uppercase tracking-[0.28em] font-black">Leader Fox</div> 2130 </a> 2131 `} 2132 <div class="p-6"> 2133 ${date ? `<div class="text-[11px] uppercase tracking-[0.22em] font-black text-yellow-500 mb-3">${date}</div>` : ''} 2134 <h3 class="text-xl font-black tracking-tight text-zinc-900 mb-3 leading-tight"> 2135 <a href="${url}" target="_blank" rel="noopener" class="hover:text-yellow-600 transition">${title}</a> 2136 </h3> 2137 ${text ? `<p class="text-sm text-zinc-600 leading-relaxed mb-5">${text}</p>` : ''} 2138 <a href="${url}" target="_blank" rel="noopener" class="inline-flex text-sm font-black uppercase tracking-[0.18em] text-zinc-900 hover:text-yellow-600 transition"> 2139 Read more â 2140 </a> 2141 </div> 2142 </article> 2143 `; 2144 } 2145 2146 async function initBlogPreview() { 2147 const $blogPreview = document.getElementById('blog-preview'); 2148 const $blogFallback = document.getElementById('blog-fallback'); 2149 2150 if (!$blogPreview) return; 2151 2152 try { 2153 const response = await fetch('blog-posts-en.json?v=' + Date.now()); 2154 2155 if (!response.ok) { 2156 throw new Error('The blog-posts-en.json file was not found.'); 2157 } 2158 2159 const posts = await response.json(); 2160 const visiblePosts = Array.isArray(posts) 2161 ? posts.filter(post => post && post.title && post.url).slice(0, 3) 2162 : []; 2163 2164 if (!visiblePosts.length) { 2165 throw new Error('The blog-posts-en.json file does not contain any articles.'); 2166 } 2167 2168 $blogPreview.innerHTML = visiblePosts.map(blogCardHtml).join(''); 2169 if ($blogFallback) $blogFallback.classList.add('hidden'); 2170 } catch (e) { 2171 $blogPreview.innerHTML = ''; 2172 if ($blogFallback) $blogFallback.classList.remove('hidden'); 2173 } 2174 } 2175 2176 initGallery(); 2177 initBlogPreview(); 2178 2179 2180 initDealers(); 2181 </script>
2181 2182</body> 2183</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.