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 <title>LaserLab - QR Code Generator</title> 7 8 <!-- Basic Meta --> 9 <meta name="description" content="Your laser's best friend. Image prep, slicer for collages, ruler gen, QR code gen - everything to get your engrave on."> 10 <meta name="author" content="LaserLab"> 11 12 <!-- Open Graph (Facebook, LinkedIn, etc.) --> 13 <meta property="og:type" content="website"> 14 <meta property="og:url" content="https://laserlab.app/"> 15 <meta property="og:title" content="LaserLab - Your Laser's Best Friend"> 16 <meta property="og:description" content="Your laser's best friend. Image prep, slicer for collages, ruler gen, QR code gen - everything to get your engrave on."> 17 <meta property="og:image" content="https://laserlab.app/static/images/og-image.jpg"> 18 <meta property="og:image:width" content="1200"> 19 <meta property="og:image:height" content="630"> 20 <meta property="og:site_name" content="LaserLab"> 21 22 <!-- Twitter Card --> 23 <meta name="twitter:card" content="summary_large_image"> 24 <meta name="twitter:title" content="LaserLab - Your Laser's Best Friend"> 25 <meta name="twitter:description" content="Your laser's best friend. Image prep, slicer for collages, ruler gen, QR code gen - everything to get your engrave on."> 26 <meta name="twitter:image" content="https://laserlab.app/static/images/og-image.jpg"> 27 28 <!-- Preconnect to CDNs for faster loading --> 29 <link rel="preconnect" href="https://cdnjs.cloudflare.com"> 30 <link rel="preconnect" href="https://cdn.jsdelivr.net"> 31 <link rel="preconnect" href="https://assets.laserlab.app"> 32 <link rel="preconnect" href="https://www.gstatic.com"> 33 <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com"> 34 <link rel="dns-prefetch" href="https://cdn.jsdelivr.net"> 35 <link rel="dns-prefetch" href="https://assets.laserlab.app"> 36 37 <!-- Favicon --> 38 <link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg?v=1775399980"> 39 <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32.png?v=1775399979"> 40 <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16.png?v=1775399979"> 41 <link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png?v=1775399979"> 42 <link rel="shortcut icon" href="/static/images/favicon.ico?v=1775399979"> 43 44 <!-- Custom CSS (load early) --> 45 <link rel="stylesheet" href="/static/css/style.css?v=1775399974?v=1786132325"> 46 47 <!-- noUiSlider CSS --> 48 <link rel="stylesheet" href="https://assets.laserlab.app/nouislider.min.css"> 49 50 51<style> 52/* QR CODE GENERATOR v2025-01-20-VCARD-NOTE */ 53* { 54 box-sizing: border-box; 55} 56 57body { 58 margin: 0; 59 padding: 0; 60 background: #0f172a; 61 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; 62 overflow: hidden; 63} 64 65/* Faded homepage in background */ 66.homepage-bg { 67 position: fixed; 68 top: 0; 69 left: 0; 70 width: 100%; 71 height: 100%; 72 opacity: 0.25; 73 pointer-events: none; 74 overflow: hidden; 75} 76 77.homepage-bg .home-header { 78 text-align: center; 79 padding: 5px 20px 5px; 80} 81 82.homepage-bg .home-logo { 83 max-width: 380px; 84 height: auto; 85 margin-top: -40px; 86 margin-bottom: -50px; 87} 88 89.homepage-bg .home-tagline { 90 color: #94a3b8; 91 font-size: 1.1rem; 92 margin: 0 0 5px; 93 font-weight: 300; 94} 95 96.homepage-bg .home-features { 97 display: flex; 98 justify-content: center; 99 gap: 25px; 100 padding: 10px 30px; 101 flex: 1; 102 align-items: flex-start; 103 flex-wrap: wrap; 104} 105 106.homepage-bg .feature-box { 107 width: 380px; 108 background: rgba(20, 30, 50, 0.8); 109 border: 2px solid #3b82f6; 110 border-radius: 8px; 111 overflow: hidden; 112 display: flex; 113 flex-direction: column; 114 box-shadow: 0 0 20px rgba(59, 130, 246, 0.3); 115} 116 117.homepage-bg .feature-box.green { 118 border-color: #22c55e; 119 box-shadow: 0 0 20px rgba(34, 197, 94, 0.3); 120} 121 122.homepage-bg .feature-box.cyan { 123 border-color: #06b6d4; 124 box-shadow: 0 0 20px rgba(6, 182, 212, 0.3); 125} 126 127.homepage-bg .feature-box.purple { 128 border-color: #8b5cf6; 129 box-shadow: 0 0 20px rgba(139, 92, 246, 0.3); 130} 131 132.homepage-bg .feature-box.mini { 133 width: 300px; 134 flex-direction: row; 135 height: auto; 136} 137 138.homepage-bg .feature-box.mini .feature-img { 139 width: 120px; 140 height: 120px; 141 object-fit: cover; 142 flex-shrink: 0; 143} 144 145.homepage-bg .feature-box.mini .feature-info { 146 padding: 12px 15px; 147} 148 149.homepage-bg .feature-img { 150 width: 100%; 151 height: 240px; 152 object-fit: cover; 153} 154 155.homepage-bg .feature-info { 156 padding: 15px 20px; 157} 158 159.homepage-bg .feature-name { 160 color: #fff; 161 font-size: 1.4rem; 162 font-weight: 600; 163 margin: 0 0 8px; 164} 165 166.homepage-bg .feature-subtitle { 167 color: #22c55e; 168 font-size: 0.8rem; 169 font-weight: 500; 170 margin: -4px 0 6px; 171} 172 173.homepage-bg .feature-desc { 174 color: #94a3b8; 175 font-size: 0.9rem; 176 line-height: 1.5; 177 margin: 0; 178} 179 180.homepage-bg .mini-tools-column { 181 display: flex; 182 flex-direction: column; 183 gap: 20px; 184} 185 186/* Backdrop with slight darkening */ 187.qr-backdrop { 188 position: fixed; 189 top: 0; 190 left: 0; 191 width: 100%; 192 height: 100%; 193 background: rgba(15, 23, 42, 0.7); 194 display: flex; 195 align-items: center; 196 justify-content: center; 197 z-index: 100; 198} 199 200/* Main popup container - LARGER */ 201.qr-popup { 202 background: #1e293b; 203 border: 1px solid #8b5cf6; 204 border-radius: 16px; 205 box-shadow: 0 0 60px rgba(139, 92, 246, 0.3); 206 width: 1060px; 207 max-width: 95vw; 208 max-height: 90vh; 209 overflow: hidden; 210 display: flex; 211 flex-direction: column; 212} 213 214/* Header */ 215.qr-header { 216 display: flex; 217 justify-content: space-between; 218 align-items: center; 219 padding: 16px 24px; 220 background: rgba(15, 23, 42, 0.8); 221 border-bottom: 1px solid #334155; 222} 223 224.qr-title { 225 color: #8b5cf6; 226 font-size: 18px; 227 font-weight: 600; 228 display: flex; 229 align-items: center; 230 gap: 8px; 231} 232 233.qr-close { 234 background: transparent; 235 border: none; 236 color: #94a3b8; 237 font-size: 24px; 238 cursor: pointer; 239 padding: 4px 8px; 240 border-radius: 4px; 241 transition: all 0.2s; 242 text-decoration: none; 243} 244 245.qr-close:hover { 246 background: rgba(239, 68, 68, 0.2); 247 color: #ef4444; 248} 249 250/* Tab Bar Wrapper */ 251.qr-tabs-wrapper { 252 position: relative; 253 background: rgba(15, 23, 42, 0.5); 254 border-bottom: 1px solid #334155; 255} 256 257/* Scroll hint - hidden on desktop */ 258.tabs-scroll-hint { 259 display: none; 260} 261 262/* Tab Bar - ImagePrep style */ 263.qr-tabs { 264 display: flex; 265 gap: 8px; 266 padding: 16px 24px; 267 justify-content: center; 268} 269 270.qr-tab { 271 padding: 12px 28px; 272 font-size: 13px; 273 font-weight: 600; 274 letter-spacing: 1px; 275 text-transform: uppercase;
276 border: 1px solid rgba(148, 163, 184, 0.2); 277 background: rgba(30, 41, 59, 0.6); 278 color: #cbd5e1; 279 border-radius: 6px; 280 cursor: pointer; 281 transition: all 0.3s; 282 display: flex; 283 align-items: center; 284 gap: 8px; 285} 286 287.qr-tab:hover { 288 border-color: #8b5cf6; 289 color: #ffffff; 290 background: rgba(139, 92, 246, 0.1); 291} 292 293.qr-tab.active { 294 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); 295 border-color: transparent; 296 color: #ffffff; 297 box-shadow: 0 0 20px rgba(139, 92, 246, 0.4); 298} 299 300.tab-stacked { 301 display: block; 302 text-align: center; 303 line-height: 1.2; 304 font-size: 12px; 305} 306 307/* Body - Two Column Layout */ 308.qr-body { 309 display: flex; 310 padding: 24px; 311 gap: 24px; 312 overflow: hidden; 313 flex: 1; 314 min-height: 0; 315} 316 317/* Left Column - Controls (scrollable) */ 318.qr-controls { 319 width: 280px; 320 flex-shrink: 0; 321 display: flex; 322 flex-direction: column; 323 gap: 16px; 324 overflow-y: auto; 325 padding-right: 8px; 326} 327 328/* Scrollbar styling for controls */ 329.qr-controls::-webkit-scrollbar {
330 width: 6px; 331} 332 333.qr-controls::-webkit-scrollbar-track { 334 background: rgba(15, 23, 42, 0.5); 335 border-radius: 3px; 336} 337 338.qr-controls::-webkit-scrollbar-thumb { 339 background: rgba(139, 92, 246, 0.4); 340 border-radius: 3px; 341} 342 343.qr-controls::-webkit-scrollbar-thumb:hover { 344 background: rgba(139, 92, 246, 0.6); 345} 346 347/* Tab Content Panels */ 348.tab-content { 349 display: none; 350} 351 352.tab-content.active { 353 display: block; 354} 355 356/* Control Groups */ 357.control-group { 358 background: rgba(30, 41, 59, 0.5); 359 border: 1px solid #334155; 360 border-radius: 10px; 361 padding: 14px; 362} 363 364.control-label { 365 color: #bae6fd; 366 font-size: 11px; 367 font-weight: 600; 368 text-transform: uppercase; 369 letter-spacing: 0.5px; 370 margin-bottom: 10px; 371 display: flex; 372 align-items: center; 373 gap: 6px; 374} 375 376.control-label .label-icon { 377 font-size: 13px; 378} 379 380/* Input Styles */ 381.qr-input { 382 width: 100%; 383 padding: 10px 12px; 384 background: #1e293b; 385 border: 1px solid #475569; 386 border-radius: 6px; 387 color: #e2e8f0; 388 font-size: 13px; 389 transition: border-color 0.2s; 390} 391 392.qr-input:focus { 393 outline: none; 394 border-color: #8b5cf6; 395} 396 397.qr-input::placeholder { 398 color: #64748b; 399} 400 401textarea.qr-input { 402 resize: vertical; 403 min-height: 80px; 404 font-family: inherit; 405} 406 407.input-row { 408 display: flex; 409 align-items: center; 410 gap: 8px; 411} 412 413.input-row .qr-input { 414 flex: 1; 415} 416 417.input-suffix { 418 color: #64748b; 419 font-size: 13px; 420 font-weight: 500; 421} 422 423/* Section Divider */ 424.section-divider { 425 height: 1px; 426 background: #334155; 427 margin: 4px 0; 428} 429 430/* Style Section Header */ 431.style-header { 432 color: #64748b; 433 font-size: 10px; 434 font-weight: 600; 435 text-transform: uppercase; 436 letter-spacing: 1px; 437 margin-bottom: 12px; 438 padding-bottom: 8px; 439 border-bottom: 1px solid #334155; 440} 441 442/* Shape Buttons */ 443.shape-btns { 444 display: flex; 445 gap: 8px; 446} 447 448.shape-btn { 449 flex: 1; 450 padding: 10px 8px; 451 background: transparent; 452 border: 1px solid #475569; 453 color: #94a3b8; 454 border-radius: 8px; 455 cursor: pointer; 456 font-size: 12px; 457 font-weight: 500; 458 transition: all 0.2s; 459 display: flex; 460 flex-direction: column; 461 align-items: center; 462 gap: 4px; 463} 464 465.shape-btn .shape-icon { 466 font-size: 18px; 467} 468 469.shape-btn.active { 470 background: rgba(139, 92, 246, 0.15); 471 border-color: #8b5cf6; 472 color: #c4b5fd; 473} 474 475.shape-btn:hover:not(.active) { 476 border-color: #8b5cf6; 477 color: #e2e8f0; 478} 479 480/* Format Toggle */ 481.format-toggle { 482 display: flex; 483 background: #0f172a; 484 border-radius: 6px; 485 padding: 3px; 486} 487 488.format-btn { 489 flex: 1; 490 padding: 8px 12px; 491 background: transparent; 492 border: none; 493 color: #64748b; 494 border-radius: 4px; 495 cursor: pointer; 496 font-size: 12px; 497 font-weight: 600; 498 transition: all 0.2s; 499} 500 501.format-btn.active { 502 background: #8b5cf6; 503 color: white; 504} 505 506.format-btn:hover:not(.active) { 507 color: #e2e8f0; 508} 509 510/* Password Input with Toggle */ 511.password-input-wrapper { 512 position: relative; 513 display: flex; 514 align-items: center; 515} 516 517.password-input-wrapper .qr-input { 518 padding-right: 40px; 519} 520 521.password-toggle { 522 position: absolute; 523 right: 8px; 524 background: none; 525 border: none; 526 cursor: pointer; 527 padding: 4px; 528 opacity: 0.6; 529 transition: opacity 0.2s; 530} 531 532.password-toggle:hover { 533 opacity: 1; 534} 535 536.password-toggle .eye-icon { 537 font-size: 16px; 538} 539 540/* Security Type Buttons */ 541.security-btns { 542 display: flex; 543 gap: 6px; 544} 545 546.security-btn { 547 flex: 1; 548 padding: 10px 8px; 549 background: transparent; 550 border: 1px solid #475569; 551 color: #94a3b8; 552 border-radius: 6px; 553 cursor: pointer; 554 font-size: 11px; 555 font-weight: 600; 556 transition: all 0.2s; 557} 558 559.security-btn.active { 560 background: rgba(139, 92, 246, 0.15); 561 border-color: #8b5cf6; 562 color: #c4b5fd; 563} 564 565.security-btn:hover:not(.active) { 566 border-color: #8b5cf6; 567 color: #e2e8f0; 568} 569 570/* Display As Buttons (vCard) */ 571.display-as-btns { 572 display: flex; 573 gap: 6px; 574} 575 576.display-as-btn { 577 flex: 1; 578 padding: 10px 12px; 579 background: transparent; 580 border: 1px solid #475569; 581 color: #94a3b8; 582 border-radius: 6px; 583 cursor: pointer; 584 font-size: 12px; 585 font-weight: 600; 586 transition: all 0.2s; 587} 588 589.display-as-btn.active { 590 background: rgba(139, 92, 246, 0.15); 591 border-color: #8b5cf6; 592 color: #c4b5fd; 593} 594 595.display-as-btn:hover:not(.active) { 596 border-color: #8b5cf6; 597 color: #e2e8f0; 598} 599 600/* vCard fields */ 601.vcard-fields-scroll { 602 max-height: 320px; 603 overflow-y: auto; 604 padding-right: 4px; 605} 606 607.vcard-fields-scroll::-webkit-scrollbar {
608 width: 6px; 609} 610 611.vcard-fields-scroll::-webkit-scrollbar-track { 612 background: #1e293b; 613 border-radius: 3px; 614} 615 616.vcard-fields-scroll::-webkit-scrollbar-thumb { 617 background: #475569; 618 border-radius: 3px; 619} 620 621.vcard-row { 622 display: flex; 623 gap: 8px; 624} 625 626.vcard-row .qr-input { 627 flex: 1; 628} 629 630/* Checkbox styling */ 631.checkbox-row { 632 display: flex; 633 align-items: center; 634 justify-content: space-between; 635} 636 637.checkbox-label { 638 color: #94a3b8; 639 font-size: 13px; 640} 641 642.checkbox-hint { 643 color: #64748b; 644 font-size: 10px; 645 margin-top: 6px; 646} 647 648.toggle-switch { 649 position: relative; 650 width: 44px; 651 height: 24px; 652} 653 654.toggle-switch input { 655 opacity: 0; 656 width: 0; 657 height: 0; 658} 659 660.toggle-slider { 661 position: absolute; 662 cursor: pointer; 663 top: 0; 664 left: 0; 665 right: 0; 666 bottom: 0; 667 background-color: #475569; 668 transition: 0.3s; 669 border-radius: 24px; 670} 671 672.toggle-slider:before { 673 position: absolute; 674 content: ""; 675 height: 18px; 676 width: 18px; 677 left: 3px; 678 bottom: 3px; 679 background-color: white; 680 transition: 0.3s; 681 border-radius: 50%; 682} 683 684input:checked + .toggle-slider { 685 background-color: #8b5cf6; 686} 687 688input:checked + .toggle-slider:before { 689 transform: translateX(20px); 690} 691 692/* Help Icon */ 693.help-icon { 694 display: inline-flex; 695 align-items: center; 696 justify-content: center; 697 width: 16px; 698 height: 16px; 699 background: #475569; 700 border: none; 701 border-radius: 50%; 702 color: #e2e8f0; 703 font-size: 10px; 704 font-weight: 700; 705 cursor: pointer; 706 margin-left: 6px; 707 transition: all 0.2s; 708 font-family: inherit; 709} 710 711.help-icon:hover { 712 background: #8b5cf6; 713 transform: scale(1.1); 714} 715 716/* Input hint text */ 717.input-hint { 718 color: #64748b; 719 font-size: 10px; 720 margin-top: 6px; 721} 722 723/* Review URL preview */ 724.review-preview-url { 725 margin-top: 12px; 726 padding: 10px 12px; 727 background: rgba(15, 23, 42, 0.5); 728 border: 1px solid #334155; 729 border-radius: 6px; 730 color: #64748b; 731 font-size: 11px; 732 word-break: break-all; 733 line-height: 1.4; 734} 735 736.review-preview-url.valid { 737 color: #22c55e; 738 border-color: rgba(34, 197, 94, 0.3); 739 background: rgba(34, 197, 94, 0.1); 740} 741 742/* Review Info Link */ 743.review-info-link { 744 display: flex; 745 align-items: center; 746 gap: 6px; 747 padding: 10px 12px; 748 margin-bottom: 16px; 749 background: rgba(139, 92, 246, 0.1); 750 border: 1px solid rgba(139, 92, 246, 0.3); 751 border-radius: 8px; 752 color: #a78bfa; 753 font-size: 13px; 754 cursor: pointer; 755 transition: all 0.2s ease; 756} 757 758.review-info-link:hover { 759 background: rgba(139, 92, 246, 0.2); 760 border-color: rgba(139, 92, 246, 0.5); 761} 762 763.review-info-link .info-link-action { 764 color: #8b5cf6; 765 font-weight: 500; 766} 767 768/* Place ID Help Modal */ 769.placeid-help-modal { 770 display: none; 771 position: fixed; 772 top: 0; 773 left: 0; 774 width: 100%; 775 height: 100%; 776 background: rgba(0, 0, 0, 0.85); 777 z-index: 3000; 778 justify-content: center; 779 align-items: center; 780 padding: 20px; 781} 782 783.placeid-help-content { 784 background: #1e293b; 785 border: 1px solid #8b5cf6; 786 border-radius: 12px; 787 max-width: 520px; 788 width: 100%; 789 max-height: 85vh; 790 overflow-y: auto; 791 box-shadow: 0 0 40px rgba(139, 92, 246, 0.3); 792} 793 794.placeid-help-header { 795 display: flex; 796 justify-content: space-between; 797 align-items: center; 798 padding: 16px 20px; 799 border-bottom: 1px solid #334155; 800 background: rgba(15, 23, 42, 0.5); 801} 802 803.placeid-help-title { 804 color: #8b5cf6; 805 font-size: 16px; 806 font-weight: 600; 807 display: flex; 808 align-items: center; 809 gap: 8px; 810} 811 812.placeid-help-close { 813 background: transparent; 814 border: none; 815 color: #94a3b8; 816 font-size: 20px; 817 cursor: pointer; 818 padding: 4px 8px; 819 border-radius: 4px; 820 transition: all 0.2s; 821} 822 823.placeid-help-close:hover { 824 background: rgba(239, 68, 68, 0.2); 825 color: #ef4444; 826} 827 828.placeid-help-body { 829 padding: 20px; 830} 831 832.placeid-step { 833 display: flex; 834 align-items: flex-start; 835 gap: 12px; 836 margin-bottom: 16px; 837 padding-bottom: 16px; 838 border-bottom: 1px solid #334155; 839} 840 841.placeid-step:last-of-type { 842 margin-bottom: 0; 843 padding-bottom: 0; 844 border-bottom: none; 845} 846 847.placeid-step-num { 848 background: #8b5cf6; 849 color: white; 850 width: 26px;
851 height: 26px; 852 border-radius: 50%; 853 display: flex; 854 align-items: center; 855 justify-content: center; 856 font-size: 13px; 857 font-weight: bold; 858 flex-shrink: 0; 859} 860 861.placeid-step-content { 862 flex: 1; 863} 864 865.placeid-step-content h4 { 866 color: #e2e8f0; 867 font-size: 13px; 868 font-weight: 600; 869 margin: 0 0 6px; 870} 871 872.placeid-step-content p { 873 color: #94a3b8; 874 font-size: 12px; 875 line-height: 1.5; 876 margin: 0; 877} 878 879.placeid-step-content a { 880 color: #8b5cf6; 881 text-decoration: none; 882} 883 884.placeid-step-content a:hover { 885 text-decoration: underline; 886} 887 888.placeid-url-link { 889 display: block; 890 margin-top: 8px; 891 padding: 10px 14px; 892 background: rgba(139, 92, 246, 0.15); 893 border: 1px solid rgba(139, 92, 246, 0.3); 894 border-radius: 6px; 895 color: #c4b5fd !important; 896 font-size: 12px; 897 font-family: monospace; 898 word-break: break-all; 899 transition: all 0.2s; 900} 901 902.placeid-url-link:hover { 903 background: rgba(139, 92, 246, 0.25); 904 border-color: #8b5cf6; 905 text-decoration: none !important; 906} 907 908.placeid-step-content code { 909 background: rgba(139, 92, 246, 0.2); 910 color: #c4b5fd; 911 padding: 2px 6px; 912 border-radius: 3px; 913 font-size: 11px; 914 font-family: monospace; 915} 916 917.placeid-example { 918 margin-top: 16px; 919 padding: 14px; 920 background: rgba(34, 197, 94, 0.1); 921 border: 1px solid rgba(34, 197, 94, 0.3); 922 border-radius: 8px; 923} 924 925.placeid-example-title { 926 color: #22c55e; 927 font-size: 12px; 928 font-weight: 600; 929 margin-bottom: 8px; 930 display: flex; 931 align-items: center; 932 gap: 6px; 933} 934 935.placeid-example-text { 936 color: #e2e8f0; 937 font-size: 12px; 938 font-family: monospace; 939 word-break: break-all; 940} 941 942.placeid-btn-row { 943 margin-top: 16px; 944 display: flex; 945 gap: 10px; 946} 947 948.placeid-open-btn { 949 flex: 1; 950 padding: 12px; 951 background: linear-gradient(135deg, #8b5cf6, #6366f1); 952 border: none; 953 border-radius: 8px; 954 color: white; 955 font-size: 13px; 956 font-weight: 600; 957 cursor: pointer; 958 transition: all 0.2s; 959 text-decoration: none; 960 text-align: center; 961 display: flex; 962 align-items: center; 963 justify-content: center; 964 gap: 6px; 965} 966 967.placeid-open-btn:hover { 968 transform: translateY(-1px); 969 box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4); 970} 971 972.placeid-close-btn { 973 padding: 12px 20px; 974 background: transparent; 975 border: 1px solid #475569; 976 border-radius: 8px; 977 color: #94a3b8; 978 font-size: 13px; 979 font-weight: 500; 980 cursor: pointer; 981 transition: all 0.2s; 982} 983 984.placeid-close-btn:hover { 985 border-color: #8b5cf6; 986 color: #e2e8f0; 987} 988 989/* Google Places Info Modal additions */ 990.google-places-intro { 991 background: rgba(59, 130, 246, 0.1); 992 border: 1px solid rgba(59, 130, 246, 0.3); 993 border-radius: 8px; 994 padding: 12px; 995 margin-bottom: 16px; 996 color: #e2e8f0; 997 font-size: 13px; 998 line-height: 1.5; 999} 1000 1001.google-places-note { 1002 background: rgba(139, 92, 246, 0.1); 1003 border-radius: 8px; 1004 padding: 12px; 1005 margin-top: 16px; 1006 color: #c4b5fd; 1007 font-size: 12px; 1008 line-height: 1.5; 1009} 1010 1011.help-icon-inline { 1012 display: inline-flex; 1013 align-items: center; 1014 justify-content: center; 1015 width: 16px; 1016 height: 16px; 1017 background: #475569; 1018 color: #94a3b8; 1019 border-radius: 50%; 1020 font-size: 10px; 1021 font-weight: bold; 1022 vertical-align: middle; 1023} 1024 1025/* Security Help Modal */ 1026.security-help-modal { 1027 display: none; 1028 position: fixed; 1029 top: 0; 1030 left: 0; 1031 width: 100%; 1032 height: 100%; 1033 background: rgba(0, 0, 0, 0.85); 1034 z-index: 3000; 1035 justify-content: center; 1036 align-items: center; 1037 padding: 20px; 1038} 1039 1040.security-help-content { 1041 background: #1e293b; 1042 border: 1px solid #8b5cf6; 1043 border-radius: 12px; 1044 max-width: 480px; 1045 width: 100%; 1046 max-height: 85vh; 1047 overflow-y: auto; 1048 box-shadow: 0 0 40px rgba(139, 92, 246, 0.3); 1049} 1050 1051.security-help-header { 1052 display: flex; 1053 justify-content: space-between; 1054 align-items: center; 1055 padding: 16px 20px; 1056 border-bottom: 1px solid #334155; 1057 position: sticky; 1058 top: 0; 1059 background: #1e293b; 1060} 1061 1062.security-help-title { 1063 color: #8b5cf6; 1064 font-size: 16px; 1065 font-weight: 600; 1066 display: flex; 1067 align-items: center; 1068 gap: 8px; 1069} 1070 1071.security-help-close { 1072 background: transparent; 1073 border: none; 1074 color: #94a3b8; 1075 font-size: 20px; 1076 cursor: pointer; 1077 padding: 4px 8px; 1078 border-radius: 4px; 1079 transition: all 0.2s; 1080} 1081 1082.security-help-close:hover { 1083 background: rgba(239, 68, 68, 0.2); 1084 color: #ef4444; 1085} 1086 1087.security-help-body { 1088 padding: 20px; 1089} 1090 1091.help-section { 1092 margin-bottom: 20px; 1093} 1094 1095.help-section:last-child { 1096 margin-bottom: 0; 1097} 1098 1099.help-section-title { 1100 color: #e2e8f0; 1101 font-size: 14px; 1102 font-weight: 600; 1103 margin-bottom: 10px; 1104 display: flex; 1105 align-items: center; 1106 gap: 8px; 1107} 1108 1109.help-section-title .platform-icon { 1110 font-size: 18px; 1111} 1112 1113.help-steps { 1114 background: rgba(15, 23, 42, 0.5); 1115 border-radius: 8px; 1116 padding: 12px 14px; 1117} 1118 1119.help-steps ol { 1120 margin: 0; 1121 padding-left: 20px; 1122 color: #94a3b8; 1123 font-size: 12px; 1124 line-height: 1.7; 1125} 1126 1127.help-steps li { 1128 margin-bottom: 4px; 1129} 1130 1131.help-steps li:last-child { 1132 margin-bottom: 0; 1133} 1134 1135.help-steps code { 1136 background: rgba(139, 92, 246, 0.2); 1137 color: #c4b5fd; 1138 padding: 1px 5px; 1139 border-radius: 3px; 1140 font-size: 11px; 1141} 1142 1143.help-tip { 1144 margin-top: 16px; 1145 padding: 12px 14px; 1146 background: rgba(34, 197, 94, 0.1); 1147 border: 1px solid rgba(34, 197, 94, 0.3); 1148 border-radius: 8px; 1149} 1150 1151.help-tip-title { 1152 color: #22c55e; 1153 font-size: 12px; 1154 font-weight: 600; 1155 margin-bottom: 6px; 1156 display: flex; 1157 align-items: center; 1158 gap: 6px; 1159} 1160 1161.help-tip-text { 1162 color: #94a3b8; 1163 font-size: 12px; 1164 line-height: 1.5; 1165} 1166 1167.help-note { 1168 margin-top: 16px; 1169 padding: 12px 14px; 1170 background: rgba(139, 92, 246, 0.1); 1171 border-radius: 8px; 1172 color: #c4b5fd; 1173 font-size: 11px; 1174 line-height: 1.5; 1175} 1176 1177/* Download Buttons */ 1178.download-section { 1179 margin-top: auto; 1180 padding-top: 8px; 1181} 1182 1183.download-btn { 1184 width: 100%; 1185 padding: 14px; 1186 background: linear-gradient(135deg, #8b5cf6, #6366f1); 1187 border: none; 1188 border-radius: 8px; 1189 color: white; 1190 font-size: 14px; 1191 font-weight: 600; 1192 cursor: pointer; 1193 transition: all 0.3s; 1194 display: flex; 1195 align-items: center; 1196 justify-content: center; 1197 gap: 8px; 1198} 1199 1200.download-btn:hover { 1201 transform: translateY(-2px); 1202 box-shadow: 0 5px 20px rgba(139, 92, 246, 0.4); 1203} 1204 1205.download-btn .btn-icon { 1206 font-size: 16px; 1207} 1208 1209/* Right Column - Preview */ 1210.qr-preview-area { 1211 flex: 1; 1212 display: flex; 1213 flex-direction: column; 1214 align-items: center; 1215 justify-content: center; 1216 background: rgba(15, 23, 42, 0.5); 1217 border-radius: 12px; 1218 min-height: 320px; 1219 padding: 20px; 1220} 1221 1222.preview-label { 1223 color: #64748b; 1224 font-size: 10px; 1225 font-weight: 600; 1226 text-transform: uppercase; 1227 letter-spacing: 1px; 1228 margin-bottom: 16px; 1229} 1230 1231.preview-box { 1232 background: white; 1233 border-radius: 12px; 1234 padding: 0; 1235 overflow: hidden; 1236 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 1237} 1238 1239#qr-container { 1240 width: 240px; 1241 height: 240px; 1242 background: white; 1243 display: flex; 1244 align-items: center; 1245 justify-content: center; 1246} 1247 1248.preview-info { 1249 margin-top: 16px; 1250 color: #64748b; 1251 font-size: 11px; 1252 text-align: center; 1253} 1254 1255/* How-to Modal */ 1256.howto-modal { 1257 display: none; 1258 position: fixed; 1259 top: 0; 1260 left: 0; 1261 width: 100%; 1262 height: 100%; 1263 background: rgba(0, 0, 0, 0.8); 1264 z-index: 2000; 1265 justify-content: center;
1266 align-items: center; 1267} 1268 1269.howto-content { 1270 background: #1e293b; 1271 border: 1px solid #8b5cf6; 1272 border-radius: 12px; 1273 padding: 25px; 1274 max-width: 420px; 1275 width: 90%; 1276 box-shadow: 0 0 30px rgba(139, 92, 246, 0.3); 1277} 1278 1279.howto-title { 1280 color: #8b5cf6; 1281 font-size: 18px; 1282 font-weight: 600; 1283 margin-bottom: 16px; 1284 text-align: center; 1285} 1286 1287.howto-step { 1288 display: flex; 1289 align-items: flex-start; 1290 gap: 10px; 1291 margin-bottom: 10px; 1292 color: #e2e8f0; 1293 font-size: 13px; 1294 line-height: 1.4; 1295} 1296 1297.howto-number { 1298 background: #8b5cf6; 1299 color: white; 1300 min-width: 55px; 1301 padding: 4px 8px; 1302 border-radius: 4px; 1303 display: flex; 1304 align-items: center; 1305 justify-content: center; 1306 font-size: 11px; 1307 font-weight: bold; 1308 flex-shrink: 0; 1309} 1310 1311.howto-intro { 1312 color: #94a3b8; 1313 font-size: 13px; 1314 margin-bottom: 16px; 1315 text-align: center; 1316} 1317 1318.howto-divider { 1319 height: 1px; 1320 background: #334155; 1321 margin: 16px 0; 1322} 1323 1324.howto-tip { 1325 margin-top: 12px; 1326 padding: 10px; 1327 background: rgba(139, 92, 246, 0.1); 1328 border-radius: 6px; 1329 color: #c4b5fd; 1330 font-size: 12px; 1331} 1332 1333.howto-btn { 1334 display: block; 1335 width: 100%; 1336 padding: 10px; 1337 margin-top: 16px; 1338 background: #8b5cf6; 1339 color: white; 1340 border: none; 1341 border-radius: 6px; 1342 font-size: 13px; 1343 font-weight: 500; 1344 cursor: pointer; 1345 transition: background 0.2s; 1346} 1347 1348.howto-btn:hover { 1349 background: #7c3aed; 1350} 1351 1352/* Download Modal */ 1353.download-modal { 1354 display: none; 1355 position: fixed; 1356 top: 0; 1357 left: 0; 1358 width: 100%; 1359 height: 100%; 1360 background: rgba(0, 0, 0, 0.8); 1361 z-index: 2000; 1362 justify-content: center; 1363 align-items: center; 1364} 1365 1366.download-content { 1367 text-align: center; 1368} 1369 1370.qr-building-animation { 1371 display: grid; 1372 grid-template-columns: repeat(4, 1fr); 1373 gap: 4px; 1374 width: 60px; 1375 height: 60px; 1376 margin: 0 auto 15px; 1377} 1378 1379.qr-dot { 1380 background: #8b5cf6; 1381 border-radius: 2px; 1382 animation: pulse 0.6s ease-in-out infinite alternate; 1383} 1384 1385.qr-dot:nth-child(1) { animation-delay: 0s; } 1386.qr-dot:nth-child(2) { animation-delay: 0.1s; } 1387.qr-dot:nth-child(3) { animation-delay: 0.2s; } 1388.qr-dot:nth-child(4) { animation-delay: 0.3s; } 1389.qr-dot:nth-child(5) { animation-delay: 0.1s; } 1390.qr-dot:nth-child(6) { animation-delay: 0.2s; } 1391.qr-dot:nth-child(7) { animation-delay: 0.3s; } 1392.qr-dot:nth-child(8) { animation-delay: 0.4s; } 1393.qr-dot:nth-child(9) { animation-delay: 0.2s; } 1394.qr-dot:nth-child(10) { animation-delay: 0.3s; } 1395.qr-dot:nth-child(11) { animation-delay: 0.4s; } 1396.qr-dot:nth-child(12) { animation-delay: 0.5s; } 1397.qr-dot:nth-child(13) { animation-delay: 0.3s; } 1398.qr-dot:nth-child(14) { animation-delay: 0.4s; } 1399.qr-dot:nth-child(15) { animation-delay: 0.5s; } 1400.qr-dot:nth-child(16) { animation-delay: 0.6s; } 1401 1402@keyframes pulse { 1403 0% { opacity: 0.3; transform: scale(0.8); } 1404 100% { opacity: 1; transform: scale(1); } 1405} 1406 1407.download-text { 1408 color: #e2e8f0; 1409 font-size: 15px; 1410 font-weight: 500; 1411} 1412 1413.download-dots::after { 1414 content: ''; 1415 animation: dots 1.5s steps(4, end) infinite; 1416} 1417 1418@keyframes dots { 1419 0%, 20% { content: ''; } 1420 40% { content: '.'; } 1421 60% { content: '..'; } 1422 80%, 100% { content: '...'; } 1423} 1424 1425/* Responsive */ 1426@media (max-width: 1100px) { 1427 .qr-popup { 1428 width: 95vw; 1429 } 1430} 1431 1432@media (max-width: 820px) { 1433 /* Full-screen popup on mobile */ 1434 .qr-backdrop { 1435 align-items: stretch; 1436 justify-content: stretch; 1437 padding: 0; 1438 } 1439 1440 .qr-popup { 1441 width: 100vw; 1442 max-width: 100vw; 1443 height: 100vh; 1444 max-height: 100vh; 1445 border-radius: 0; 1446 border: none; 1447 box-shadow: none; 1448 } 1449 1450 /* Compact header */ 1451 .qr-header { 1452 padding: 12px 16px; 1453 } 1454 1455 .qr-title { 1456 font-size: 16px; 1457 } 1458 1459 /* Scrollable tab bar */ 1460 .qr-tabs-wrapper { 1461 position: relative; 1462 } 1463 1464 .tabs-scroll-hint { 1465 display: block; 1466 position: absolute; 1467 top: 4px; 1468 right: 16px; 1469 font-size: 11px; 1470 color: #ffffff; 1471 letter-spacing: 0.5px; 1472 font-weight: 500; 1473 z-index: 10; 1474 } 1475 1476 .qr-tabs { 1477 padding: 12px 16px; 1478 padding-top: 22px; /* Make room for hint */ 1479 gap: 6px; 1480 overflow-x: auto; 1481 justify-content: flex-start; 1482 -webkit-overflow-scrolling: touch; 1483 scrollbar-width: none; /* Firefox */ 1484 } 1485 1486 .qr-tabs::-webkit-scrollbar {
1487 display: none; /* Chrome/Safari */ 1488 } 1489 1490 .qr-tab { 1491 padding: 10px 16px; 1492 font-size: 11px; 1493 flex-shrink: 0; 1494 white-space: nowrap; 1495 } 1496 1497 .tab-stacked { 1498 font-size: 10px; 1499 } 1500 1501 /* Scrollable body */ 1502 .qr-body { 1503 flex-direction: column; 1504 overflow-y: auto; 1505 padding: 16px; 1506 gap: 16px; 1507 flex: 1; 1508 } 1509 1510 .qr-controls { 1511 width: 100%; 1512 overflow-y: visible; 1513 max-height: none; 1514 } 1515 1516 /* Fix QR code clipping on mobile */ 1517 .qr-preview-area { 1518 min-height: auto; 1519 order: -1; /* Move preview to top on mobile */ 1520 flex-shrink: 0; 1521 padding: 20px; 1522 } 1523 1524 .preview-box { 1525 overflow: visible; 1526 padding: 15px; 1527 } 1528 /* Smaller shape buttons */ 1529 .shape-btns { 1530 gap: 6px; 1531 } 1532 1533 .shape-btn { 1534 padding: 8px 12px; 1535 font-size: 11px; 1536 } 1537 1538 /* Download button full width */ 1539 .download-btn { 1540 width: 100%; 1541 } 1542} 1543</style> 1544 1545</head> 1546<body> 1547 1548<!-- Faded Homepage Background --> 1549<div class="homepage-bg"> 1550 <nav class="top-nav"> 1551 <a href="/" class="logo">LaserLab</a> 1552 <div class="nav-links"> 1553 <a href="/">Home</a> 1554 <div class="nav-dropdown"> 1555 <button class="nav-dropdown-btn has-active"> 1556 Tools <span class="nav-dropdown-arrow">â¼</span> 1557 </button> 1558 <div class="nav-dropdown-menu"> 1559 <a href="/imageprep">ImagePrep</a> 1560 <a href="/slicer">Slicer</a> 1561 <a href="/qrcode" class="active">QR Code Generator</a> 1562 <a href="/ruler">Ruler Generator</a> 1563 </div> 1564 </div> 1565 <a href="https://www.facebook.com/tnelaserlab" target="_blank" rel="noopener" style="display: inline-flex; align-items: center; gap: 5px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path 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> Contact</a> 1566 </div> 1567 </nav> 1568 1569 <header class="home-header"> 1570 <img src="/static/images/laserlab-logo.png?v=1775399981" alt="LaserLab" class="home-logo"> 1571 <p class="home-tagline">Professional image preparation tools for laser engraving</p> 1572 </header> 1573 1574 <main class="home-features"> 1575 <div class="feature-box"> 1576 <img src="/static/images/feature-imageprep.jpg?v=1775399980" alt="ImagePrep" class="feature-img"> 1577 <div class="feature-info"> 1578 <h2 class="feature-name">ImagePrep</h2> 1579 <p class="feature-desc">Crop, Resize, Adjust, Dither images. Plus tools for removing spots, hair etc.</p> 1580 </div> 1581 </div> 1582 1583 <div class="feature-box green"> 1584 <img src="/static/images/feature-slicer.jpg?v=1775399980" alt="Slicer" class="feature-img"> 1585 <div class="feature-info"> 1586 <h2 class="feature-name">Slicer</h2> 1587 <p class="feature-desc">Slice images for multi-tile engravings with automatic gap compensation for perfect alignment.</p> 1588 </div> 1589 </div> 1590 1591 <div class="mini-tools-column"> 1592 <div class="feature-box cyan mini"> 1593 <img src="/static/images/feature-ruler.jpg?v=1775399980" alt="Ruler Generator" class="feature-img"> 1594 <div class="feature-info"> 1595 <h2 class="feature-name">Ruler Generator</h2> 1596 <p class="feature-desc">Create custom rulers. Download laser-ready SVGs with cm/inch scales.</p> 1597 </div> 1598 </div> 1599 1600 <div class="feature-box purple mini"> 1601 <img src="/static/images/feature-qrcode.jpg?v=1775399980" alt="QR Code Generator" class="feature-img"> 1602 <div class="feature-info"> 1603 <h2 class="feature-name">QR Code Generator</h2> 1604 <p class="feature-desc">URL, WiFi, vCard & Google Review QR codes.</p> 1605 </div> 1606 </div> 1607 </div> 1608 </main> 1609</div> 1610 1611<!-- Popup overlay --> 1612<div class="qr-backdrop"> 1613 <div class="qr-popup"> 1614 <!-- Header --> 1615 <div class="qr-header"> 1616 <div class="qr-title">
1617 <span>ð±</span> QR Code Generator 1618 </div> 1619 <a href="/" class="qr-close">â</a> 1620 </div> 1621 1622 <!-- Tab Bar --> 1623 <div class="qr-tabs-wrapper"> 1624 <span class="tabs-scroll-hint">more â</span> 1625 <div class="qr-tabs"> 1626 <button class="qr-tab active" data-tab="url">URL</button> 1627 <button class="qr-tab" data-tab="wifi">WIFI</button> 1628 <button class="qr-tab" data-tab="vcard">VCARD</button> 1629 <button class="qr-tab" data-tab="review"> 1630 <span class="tab-stacked">GOOGLE<br>REVIEW</span> 1631 </button> 1632 </div> 1633 </div> 1634 1635 <!-- Body --> 1636 <div class="qr-body"> 1637 <!-- Left Column - Controls --> 1638 <div class="qr-controls"> 1639 1640 <!-- URL Tab Content --> 1641 <div class="tab-content active" id="tab-url"> 1642 <div class="control-group"> 1643 <div class="control-label"> 1644 <span class="label-icon">ð</span> URL / Content 1645 </div> 1646 <textarea class="qr-input" id="qr-content" placeholder="Enter URL or text...">https://laserlab.app</textarea> 1647 </div> 1648 </div> 1649 1650 <!-- WiFi Tab Content --> 1651 <div class="tab-content" id="tab-wifi"> 1652 <div class="control-group"> 1653 <div class="control-label"> 1654 <span class="label-icon">ð¶</span> Network Name (SSID) 1655 </div> 1656 <input type="text" class="qr-input" id="wifi-ssid" placeholder="Enter network name"> 1657 </div> 1658 1659 <div class="control-group"> 1660 <div class="control-label"> 1661 <span class="label-icon">ð</span> Password 1662 </div> 1663 <div class="password-input-wrapper"> 1664 <input type="password" class="qr-input" id="wifi-p
1664assword" placeholder="Enter password (if required)"> 1665 <button type="button" class="password-toggle" id="password-toggle" title="Show/Hide password"> 1666 <span class="eye-icon">ð</span> 1667 </button> 1668 </div> 1669 </div> 1670 1671 <div class="control-group"> 1672 <div class="control-label"> 1673 <span class="label-icon">ð</span> Security Type 1674 <button type="button" class="help-icon" id="security-help-btn" title="How to find your security type">?</button> 1675 </div> 1676 <div class="security-btns"> 1677 <button class="security-btn active" data-security="WPA">WPA/WPA2/WPA3</button> 1678 <button class="security-btn" data-security="WEP">WEP</button> 1679 <button class="security-btn" data-security="nopass">None</button> 1680 </div> 1681 </div> 1682 1683 <div class="control-group"> 1684 <div class="checkbox-row"> 1685 <span class="checkbox-label">Hidden Network</span> 1686 <label class="toggle-switch"> 1687 <input type="checkbox" id="wifi-hidden"> 1688 <span class="toggle-slider"></span> 1689 </label> 1690 </div> 1691 <div class="checkbox-hint">Enable if network doesn't broadcast its name</div> 1692 </div> 1693 </div> 1694 1695 <!-- vCard Tab Content --> 1696 <div class="tab-content" id="tab-vcard"> 1697 <div class="control-group"> 1698 <div class="control-label"> 1699 <span class="label-icon">ð¤</span> Display As 1700 </div> 1701 <div class="display-as-btns"> 1702 <button class="display-as-btn active" data-display="name">Name</button> 1703 <button class="display-as-btn" data-display="company">Company Name</button> 1704 </div> 1705 </div> 1706 1707 <div class="vcard-fields-scroll"> 1708 <div class="control-group vcard-name-fields"> 1709 <div class="control-label"> 1710 <span class="label-icon">âï¸</span> Name 1711 </div> 1712 <div class="vcard-row"> 1713 <input type="text" class="qr-input" id="vcard-firstname" placeholder="First name"> 1714 <input type="text" class="qr-input" id="vcard-lastname" placeholder="Last name"> 1715 </div> 1716 </div> 1717 1718 <div class="control-group"> 1719 <div class="control-label"> 1720 <span class="label-icon">ð¢</span> Company 1721 </div> 1722 <input type="text" class="qr-input" id="vcard-company" placeholder="Company name"> 1723 </div> 1724 1725 <div class="control-group vcard-name-fields"> 1726 <div class="control-label"> 1727 <span class="label-icon">ð¼</span> Job Title 1728 </div> 1729 <input type="text" class="qr-input" id="vcard-title" placeholder="Job title"> 1730 </div> 1731 1732 <div class="control-group"> 1733 <div class="control-label"> 1734 <span class="label-icon">ð±</span> Phone 1735 </div> 1736 <div class="vcard-row"> 1737 <input type="tel" class="qr-input" id="vcard-mobile" placeholder="Mobile"> 1738 <input type="tel" class="qr-input" id="vcard-work" placeholder="Work"> 1739 </div> 1740 </div> 1741 1742 <div class="control-group"> 1743 <div class="control-label">
1744 <span class="label-icon">âï¸</span> Email 1745 </div> 1746 <input type="email" class="qr-input" id="vcard-email" placeholder="[email protected]"> 1747 </div> 1748 1749 <div class="control-group"> 1750 <div class="control-label"> 1751 <span class="label-icon">ð</span> Website 1752 </div> 1753 <input type="url" class="qr-input" id="vcard-website" placeholder="https://example.com"> 1754 </div> 1755 1756 <div class="control-group"> 1757 <div class="control-label"> 1758 <span class="label-icon">ð</span> Address 1759 </div> 1760 <input type="text" class="qr-input" id="vcard-street" placeholder="Street address"> 1761 <div class="vcard-row" style="margin-top:8px;"> 1762 <input type="text" class="qr-input" id="vcard-city" placeholder="City"> 1763 <input type="text" class="qr-input" id="vcard-postcode" placeholder="Postcode"> 1764 </div> 1765 <input type="text" class="qr-input" id="vcard-country" placeholder="Country" style="margin-top:8px;"> 1766 </div> 1767 </div> 1768 </div> 1769 1770 <!-- Review Tab Content --> 1771 <div class="tab-content" id="tab-review"> 1772 <div class="review-info-link" id="show-google-places-info"> 1773 <span>â¹ï¸</span> New to Google Reviews? <span class="info-link-action">Learn how to get started â</span> 1774 </div> 1775 1776 <div class="control-group"> 1777 <div class="control-label"> 1778 <span class="label-icon">ðª</span> Business Name 1779 </div> 1780 <input type="text" class="qr-input" id="review-business" placeholder="e.g. Nia's Diner"> 1781 <div class="input-hint">Optional - for your reference only</div> 1782 </div> 1783 1784 <div class="control-group"> 1785 <div class="control-label"> 1786 <span class="label-icon">ð</span> Google Place ID 1787 <button type="button" class="help-icon" id="placeid-help-btn" title="How to find your Place ID">?</button> 1788 </div> 1789 <input type="text" class="qr-input" id="review-placeid" placeholder="ChIJ..."> 1790 <div class="input-hint">Required - starts with "ChIJ"</div> 1791 </div> 1792 1793 <div class="review-preview-url" id="review-url-preview"> 1794 Enter Place ID to generate review link 1795 </div> 1796 </div> 1797 <div class="section-divider"></div> 1798 1799 <!-- Size --> 1800 <div class="control-group"> 1801 <div class="control-label"> 1802 <span class="label-icon">ð</span> Size 1803 </div> 1804 <div class="input-row"> 1805 <input type="number" class="qr-input" id="qr-size" value="77" min="10" max="300"> 1806 <span class="input-suffix">mm</span> 1807 </div> 1808 </div> 1809 1810 <!-- Style Section --> 1811 <div class="control-group"> 1812 <div class="style-header">Style Options</div> 1813 1814 <!-- Shape --> 1815 <div class="control-label"> 1816 <span class="label-icon">ð·</span> Shape 1817 </div> 1818 <div class="shape-btns"> 1819 <!-- Blocky house for URL tab only --> 1820 <button class="shape-btn shape-url-only" data-shape="house" title="House - blocky outline">
1821 <span class="shape-icon">ð </span> 1822 House 1823 </button> 1824 <!-- WiFi houses for WiFi tab only --> 1825 <button class="shape-btn shape-wifi-only" data-shape="houseA" title="House A - WiFi text at top" style="display:none;"> 1826 <span class="shape-icon">ð </span> 1827 House A 1828 </button> 1829 <button class="shape-btn shape-wifi-only" data-shape="houseB" title="House B - WiFi text at bottom" style="display:none;"> 1830 <span class="shape-icon">ð¡</span> 1831 House B 1832 </button> 1833 <button class="shape-btn active" data-shape="square"> 1834 <span class="shape-icon">â¼</span> 1835 Square 1836 </button> 1837 <button class="shape-btn" data-shape="circle"> 1838 <span class="shape-icon">â</span> 1839 Circle 1840 </button> 1841 </div> 1842 </div> 1843 1844 <!-- Format --> 1845 <div class="control-group"> 1846 <div class="control-label"> 1847 <span class="label-icon">ð¾</span> Format 1848 </div> 1849 <div class="format-toggle"> 1850 <button class="format-btn active" data-format="svg">SVG</button> 1851 <button class="format-btn" data-format="png">PNG</button> 1852 </div> 1853 </div> 1854 1855 <!-- Download --> 1856 <div class="download-section"> 1857 <button class="download-btn" id="download-btn"> 1858 <span class="btn-icon">â¬</span> Download 1859 </button> 1860 </div> 1861 1862 </div> 1863 1864 <!-- Right Column - Preview --> 1865 <div class="qr-preview-area"> 1866 <div class="preview-label">Preview</div> 1867 <div class="preview-box"> 1868 <div id="qr-container"></div> 1869 </div> 1870 <div class="preview-info" id="preview-info"> 1871 77mm à 77mm ⢠SVG 1872 </div> 1873 </div> 1874 </div> 1875 </div> 1876</div> 1877 1878<!-- How-to Modal --> 1879<div id="howto-modal" class="howto-modal"> 1880 <div class="howto-content"> 1881 <div class="howto-title">ð± QR Code Generator</div> 1882 1883 <div class="howto-intro">Create laser-ready QR codes in multiple formats:</div> 1884 1885 <div class="howto-step"> 1886 <div class="howto-number">URL</div> 1887 <div>Website links or plain text</div> 1888 </div> 1889 1890 <div class="howto-step"> 1891 <div class="howto-number">WIFI</div> 1892 <div>Network credentials - scan to connect</div> 1893 </div> 1894 1895 <div class="howto-step"> 1896 <div class="howto-number">VCARD</div> 1897 <div>Contact cards - scan to save to phone</div> 1898 </div> 1899 1900 <div class="howto-step"> 1901 <div class="howto-number" style="font-size:9px;padding:6px 4px;">REVIEW</div> 1902 <div>Google Review links for your business</div> 1903 </div> 1904 1905 <div class="howto-divider"></div> 1906 1907 <div class="howto-tip"> 1908 <strong>Shapes:</strong> Square, Circle, or House outlines<br> 1909 <strong>Formats:</strong> SVG or PNG with embedded DPI 1910 </div> 1911 1912 <button class="howto-btn" id="howto-close-btn">Got it!</button> 1913 </div> 1914</div> 1915 1916<!-- Download Modal --> 1917<div id="download-modal" class="download-modal"> 1918 <div class="download-content"> 1919 <div class="qr-building-animation"> 1920 <div class="qr-dot"></div> 1921 <div class="qr-dot"></div> 1922 <div class="qr-dot"></div> 1923 <div class="qr-dot"></div> 1924 <div class="qr-dot"></div> 1925 <div class="qr-dot"></div> 1926 <div class="qr-dot"></div> 1927 <div class="qr-dot"></div> 1928 <div class="qr-dot"></div> 1929 <div class="qr-dot"></div> 1930 <div class="qr-dot"></div> 1931 <div class="qr-dot"></div> 1932 <div class="qr-dot"></div> 1933 <div class="qr-dot"></div> 1934 <div class="qr-dot"></div> 1935 <div class="qr-dot"></div> 1936 </div> 1937 <div class="download-text">Building QR Code<span class="download-dots"></span></div> 1938 </div> 1939</div> 1940 1941<!-- Security Type Help Modal --> 1942<div id="security-help-modal" class="security-help-modal"> 1943 <div class="security-help-content"> 1944 <div class="security-help-header"> 1945 <div class="security-help-title">
1946 <span>ð</span> How to Find Your WiFi Security Type 1947 </div> 1948 <button class="security-help-close" id="security-help-close">â</button> 1949 </div> 1950 <div class="security-help-body"> 1951 1952 <div class="help-section"> 1953 <div class="help-section-title"> 1954 <span class="platform-icon">ð</span> iPhone / iPad 1955 </div> 1956 <div class="help-steps"> 1957 <ol> 1958 <li>Open <code>Settings</code></li> 1959 <li>Tap <code>Wi-Fi</code></li> 1960 <li>Tap the <code>â</code> icon next to your connected network</li> 1961 <li>Look for "Security" â it will show WPA2, WPA3, WEP, or None</li> 1962 </ol> 1963 </div> 1964 </div> 1965 1966 <div class="help-section"> 1967 <div class="help-section-title"> 1968 <span class="platform-icon">ð¤</span> Android 1969 </div> 1970 <div class="help-steps"> 1971 <ol> 1972 <li>Open <code>Settings</code></li> 1973 <li>Tap <code>Network & Internet</code> â <code>Wi-Fi</code></li> 1974 <li>Tap on your connected network name</li> 1975 <li>Look for "Security" â shows WPA2, WPA3, WEP, or Open</li> 1976 </ol> 1977 </div> 1978 </div> 1979 1980 <div class="help-section"> 1981 <div class="help-section-title"> 1982 <span class="platform-icon">ðª</span> Windows 10/11 1983 </div> 1984 <div class="help-steps"> 1985 <ol> 1986 <li>Click the WiFi icon in taskbar</li> 1987 <li>Click <code>Properties</code> under your network</li> 1988 <li>Scroll down to find "Security type"</li> 1989 <li>Shows WPA2-Personal, WPA3, WEP, or Open</li> 1990 </ol> 1991 </div> 1992 </div> 1993 1994 <div class="help-section"> 1995 <div class="help-section-title"> 1996 <span class="platform-icon">ð</span> Mac 1997 </div> 1998 <div class="help-steps"> 1999 <ol> 2000 <li>Hold <code>Option</code> key and click WiFi icon in menu bar</li> 2001 <li>Look for "Security" in the dropdown info</li> 2002 <li>Shows WPA2 Personal, WPA3, WEP, or None</li> 2003 </ol> 2004 </div> 2005 </div> 2006 2007 <div class="help-tip"> 2008 <div class="help-tip-title"> 2009 <span>ð¦</span> Check Your Router 2010 </div> 2011 <div class="help-tip-text"> 2012 Most routers have a label on the back or bottom showing the default WiFi name (SSID), password, and security type. Look for "Security: WPA2" or similar. 2013 </div> 2014 </div> 2015 2016 <div class="help-note"> 2017 <strong>ð¡ Quick Guide:</strong><br> 2018 ⢠<strong>WPA/WPA2/WPA3</strong> â Most common, works with all modern routers<br> 2019 ⢠<strong>WEP</strong> â Older security, rarely used today<br> 2020 ⢠<strong>Open/None</strong> â No password required (public WiFi) 2021 </div> 2022 2023 </div> 2024 </div> 2025</div> 2026 2027<!-- Place ID Help Modal --> 2028<div id="placeid-help-modal" class="placeid-help-modal"> 2029 <div class="placeid-help-content"> 2030 <div class="placeid-help-header"> 2031 <div class="placeid-help-title"> 2032 <span>ð</span> How to Find Your Google Place ID 2033 </div> 2034 <button class="placeid-help-close" id="placeid-help-close">â</button> 2035 </div> 2036 <div class="placeid-help-body"> 2037 2038 <div class="placeid-step"> 2039 <div class="placeid-step-num">1</div> 2040 <div class="placeid-step-content"> 2041 <h4>Open Google's Place ID Finder</h4> 2042 <p>Click the link below to open Google's official tool:</p> 2043 <a href="https://developers.google.com/maps/documentation/javascript/examples/places-placeid-finder" target="_blank" rel="noopener" class="placeid-url-link"> 2044 developers.google.com/maps/.../places-placeid-finder â 2045 </a> 2046 </div> 2047 </div> 2048 2049 <div class="placeid-step"> 2050 <div class="placeid-step-num">2</div> 2051 <div class="placeid-step-content"> 2052 <h4>Search for Your Business</h4> 2053 <p>Type your business name in the search box on the map. Include your city/location for accurate results.</p> 2054 </div> 2055 </div> 2056 2057 <div class="placeid-step"> 2058 <div class="placeid-step-num">3</div> 2059 <div class="placeid-step-content"> 2060 <h4>Click on Your Business</h4> 2061 <p>Click on your business in the search results or on the map marker. A popup will appear with details.</p> 2062 </div> 2063 </div> 2064 2065 <div class="placeid-step"> 2066 <div class="placeid-step-num">4</div> 2067 <div class="placeid-step-content"> 2068 <h4>Copy the Place ID</h4> 2069 <p>Find the <code>Place ID</code> field - it starts with <code>ChIJ</code>
2069. Copy this entire string.</p> 2070 </div> 2071 </div> 2072 2073 <div class="placeid-example"> 2074 <div class="placeid-example-title"> 2075 <span>â </span> Example Place ID 2076 </div> 2077 <div class="placeid-example-text">ChIJIY5V-9cXbkgRJUTnmlBP0wM</div> 2078 </div> 2079 2080 <div class="placeid-btn-row"> 2081 <button class="placeid-close-btn" id="placeid-help-close-btn" style="flex:1;">Got it!</button> 2082 </div> 2083 2084 </div> 2085 </div> 2086</div> 2087 2088<!-- Google Places Info Modal (shows on first click of Review tab) --> 2089<div id="google-places-modal" class="placeid-help-modal"> 2090 <div class="placeid-help-content"> 2091 <div class="placeid-help-header"> 2092 <div class="placeid-help-title"> 2093 <span>â</span> Google Review QR Codes 2094 </div> 2095 <button class="placeid-help-close" id="google-places-close">â</button> 2096 </div> 2097 <div class="placeid-help-body"> 2098 2099 <div class="google-places-intro"> 2100 To create a Google Review QR code, your business must be registered on <strong>Google Business Profile</strong> (formerly Google My Business). 2101 </div> 2102 2103 <div class="placeid-step"> 2104 <div class="placeid-step-num">?</div> 2105 <div class="placeid-step-content"> 2106 <h4>Not Sure If You're Registered?</h4> 2107 <p>Search for your business on Google Maps. If it appears with your details, you're already listed!</p> 2108 </div> 2109 </div> 2110 2111 <div class="placeid-step"> 2112 <div class="placeid-step-num">+</div> 2113 <div class="placeid-step-content"> 2114 <h4>Register Your Business</h4> 2115 <p>Create or claim your Google Business Profile:</p> 2116 <a href="https://www.google.com/business/" target="_blank" rel="noopener" class="placeid-url-link"> 2117 google.com/business â 2118 </a> 2119 </div> 2120 </div> 2121 2122 <div class="placeid-step"> 2123 <div class="placeid-step-num">ð</div> 2124 <div class="placeid-step-content"> 2125 <h4>Need Help?</h4> 2126 <p>Google's official guide to getting started:</p> 2127 <a href="https://support.google.com/business/answer/10514137" target="_blank" rel="noopener" class="placeid-url-link"> 2128 Google Business Profile Help â 2129 </a> 2130 </div> 2131 </div> 2132 2133 <div class="google-places-note"> 2134 <strong>ð¡ Once registered:</strong> Use the <span class="help-icon-inline">?</span> button next to "Google Place ID" to find your unique Place ID. 2135 </div> 2136 2137 <div class="placeid-btn-row"> 2138 <button class="placeid-close-btn" id="google-places-close-btn" style="flex:1;">Got it!</button> 2139 </div> 2140 2141 </div> 2142 </div> 2143</div> 2144
2145<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
vendor: 1 bytes, line 2145
2145
2146<script src="/static/js/qrcode-gen.js?v=1780941765"></script>
2146 2147 2148<!-- Tab switching and WiFi logic -->
2149<script> 2150document.addEventListener('DOMContentLoaded', function() { 2151 // Function to show/hide house shapes based on tab 2152 function updateShapeButtonsForTab(tabId) { 2153 const houseBtn = document.querySelector('.shape-btn[data-shape="house"]'); // Blocky house (URL) 2154 const houseABtn = document.querySelector('.shape-btn[data-shape="houseA"]'); // WiFi house A 2155 const houseBBtn = document.querySelector('.shape-btn[data-shape="houseB"]'); // WiFi house B 2156 const squareBtn = document.querySelector('.shape-btn[data-shape="square"]'); 2157 2158 if (tabId === 'url') { 2159 // URL tab: show blocky house, hide WiFi houses 2160 if (houseBtn) houseBtn.style.display = ''; 2161 if (houseABtn) houseABtn.style.display = 'none'; 2162 if (houseBBtn) houseBBtn.style.display = 'none'; 2163 2164 // If a WiFi house shape is active, switch to square 2165 const activeShape = document.querySelector('.shape-btn.active'); 2166 if (activeShape && (activeShape.dataset.shape === 'houseA' || activeShape.dataset.shape === 'houseB')) { 2167 activeShape.classList.remove('active'); 2168 if (squareBtn) { 2169 squareBtn.classList.add('active'); 2170 if (typeof window.setShape === 'function') { 2171 window.setShape('square'); 2172 } 2173 } 2174 } 2175 } else if (tabId === 'wifi') {
2176 // WiFi tab: show WiFi houses, hide blocky house 2177 if (houseBtn) houseBtn.style.display = 'none'; 2178 if (houseABtn) houseABtn.style.display = ''; 2179 if (houseBBtn) houseBBtn.style.display = ''; 2180 2181 // If blocky house is active, switch to square 2182 const activeShape = document.querySelector('.shape-btn.active'); 2183 if (activeShape && activeShape.dataset.shape === 'house') { 2184 activeShape.classList.remove('active'); 2185 if (squareBtn) { 2186 squareBtn.classList.add('active'); 2187 if (typeof window.setShape === 'function') { 2188 window.setShape('square'); 2189 } 2190 } 2191 } 2192 } else { 2193 // Review and vCard tabs: hide all house buttons 2194 if (houseBtn) houseBtn.style.display = 'none'; 2195 if (houseABtn) houseABtn.style.display = 'none'; 2196 if (houseBBtn) houseBBtn.style.display = 'none'; 2197 2198 // If any house shape is currently active, switch to square 2199 const activeShape = document.querySelector('.shape-btn.active'); 2200 if (activeShape && (activeShape.dataset.shape === 'house' || activeShape.dataset.shape === 'houseA' || activeShape.dataset.shape === 'houseB')) { 2201 activeShape.classList.remove('active'); 2202 if (squareBtn) { 2203 squareBtn.classList.add('active'); 2204 if (typeof window.setShape === 'function') { 2205 window.setShape('square'); 2206 } 2207 } 2208 } 2209 } 2210 } 2211 2212 // Tab switching 2213 document.querySelectorAll('.qr-tab').forEach(tab => { 2214 tab.addEventListener('click', function() { 2215 const tabId = this.dataset.tab; 2216 2217 // Update tab buttons 2218 document.querySelectorAll('.qr-tab').forEach(t => t.classList.remove('active')); 2219 this.classList.add('active'); 2220 2221 // Update tab content 2222 document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); 2223 document.getElementById('tab-' + tabId).classList.add('active'); 2224 2225 // Update shape buttons visibility for this tab 2226 updateShapeButtonsForTab(tabId); 2227 2228 // Show Google Places info modal on first click of Review tab 2229 if (tabId === 'review' && !localStorage.getItem('reviewTabInfoShown')) { 2230 const googlePlacesModal = document.getElementById('google-places-modal'); 2231 if (googlePlacesModal) { 2232 googlePlacesModal.style.display = 'flex'; 2233 localStorage.setItem('reviewTabInfoShown', 'true'); 2234 } 2235 } 2236 2237 // Update QR state and regenerate 2238 if (typeof updateQRFromActiveTab === 'function') { 2239 updateQRFromActiveTab(); 2240 } 2241 }); 2242 }); 2243 2244 // Password show/hide toggle 2245 const passwordToggle = document.getElementById('password-toggle'); 2246 const passwordInput = document.getElementById('wifi-password'); 2247 if (passwordToggle && passwordInput) { 2248 passwordToggle.addEventListener('click', function() { 2249 const isPassword = passwordInput.type === 'password'; 2250 passwordInput.type = isPassword ? 'text' : 'password'; 2251 this.querySelector('.eye-icon').textContent = isPassword ? 'ð' : 'ð'; 2252 }); 2253 } 2254 2255 // Security type buttons 2256 document.querySelectorAll('.security-btn').forEach(btn => { 2257 btn.addEventListener('click', function() { 2258 document.querySelectorAll('.security-btn').forEach(b => b.classList.remove('active')); 2259 this.classList.add('active'); 2260 2261 // Show/hide password field based on security type 2262 const securityType = this.dataset.security; 2263 const passwordGroup = document.getElementById('wifi-password').closest('.control-group'); 2264 if (securityType === 'nopass') { 2265 passwordGroup.style.display = 'none'; 2266 } else { 2267 passwordGroup.style.display = 'block'; 2268 } 2269 2270 // Regenerate QR 2271 if (typeof updateQRFromActiveTab === 'function') { 2272 updateQRFromActiveTab(); 2273 } 2274 }); 2275 }); 2276 2277 // WiFi inputs change handlers 2278 ['wifi-ssid', 'wifi-password'].forEach(id => { 2279 const el = document.getElementById(id); 2280 if (el) { 2281 el.addEventListener('input', function() { 2282 if (typeof updateQRFromActiveTab === 'function') { 2283 // Debounce 2284 clearTimeout(window.wifiDebounce); 2285 window.wifiDebounce = setTimeout(updateQRFromActiveTab, 300); 2286 } 2287 }); 2288 } 2289 }); 2290 2291 // Hidden network toggle 2292 const hiddenToggle = document.getElementById('wifi-hidden'); 2293 if (hiddenToggle) { 2294 hiddenToggle.addEventListener('change', function() { 2295 if (typeof updateQRFromActiveTab === 'function') { 2296 updateQRFromActiveTab(); 2297 } 2298 }); 2299 } 2300 2301 // vCard Display As buttons 2302 document.querySelectorAll('.display-as-btn').forEach(btn => { 2303 btn.addEventListener('click', function() { 2304 document.querySelectorAll('.display-as-btn').forEach(b => b.classList.remove('active')); 2305 this.classList.add('active'); 2306 if (typeof updateQRFromActiveTab === 'function') { 2307 updateQRFromActiveTab(); 2308 } 2309 }); 2310 }); 2311 2312 // vCard input change handlers 2313 const vcardInputIds = [ 2314 'vcard-firstname', 'vcard-lastname', 'vcard-company', 'vcard-title', 2315 'vcard-mobile', 'vcard-work', 'vcard-email', 'vcard-website', 2316 'vcard-street', 'vcard-city', 'vcard-postcode', 'vcard-country' 2317 ]; 2318 vcardInputIds.forEach(id => { 2319 const el = document.getElementById(id); 2320 if (el) { 2321 el.addEventListener('input', function() { 2322 if (typeof updateQRFromActiveTab === 'function') { 2323 clearTimeout(window.vcardDebounce); 2324 window.vcardDebounce = setTimeout(updateQRFromActiveTab, 300); 2325 } 2326 }); 2327 } 2328 }); 2329 2330 // Security Help Modal 2331 const securityHelpBtn = document.getElementById('security-help-btn'); 2332 const securityHelpModal = document.getElementById('security-help-modal'); 2333 const securityHelpClose = document.getElementById('security-help-close'); 2334 2335 if (securityHelpBtn && securityHelpModal) { 2336 securityHelpBtn.addEventListener('click', function(e) { 2337 e.preventDefault(); 2338 e.stopPropagation(); 2339 securityHelpModal.style.display = 'flex'; 2340 }); 2341 2342 securityHelpClose.addEventListener('click', function() { 2343 securityHelpModal.style.display = 'none'; 2344 }); 2345 2346 // Close on backdrop click 2347 securityHelpModal.addEventListener('click', function(e) { 2348 if (e.target === securityHelpModal) { 2349 securityHelpModal.style.display = 'none'; 2350 } 2351 }); 2352 2353 // Close on Escape key 2354 document.addEventListener('keydown', function(e) { 2355 if (e.key === 'Escape' && securityHelpModal.style.display === 'flex') { 2356 securityHelpModal.style.display = 'none'; 2357 } 2358 }); 2359 } 2360 2361 // Place ID Help Modal 2362 const placeidHelpBtn = document.getElementById('placeid-help-btn'); 2363 const placeidHelpModal = document.getElementById('placeid-help-modal'); 2364 const placeidHelpClose = document.getElementById('placeid-help-close'); 2365 const placeidHelpCloseBtn = document.getElementById('placeid-help-close-btn'); 2366 2367 if (placeidHelpBtn && placeidHelpModal) { 2368 placeidHelpBtn.addEventListener('click', function(e) { 2369 e.preventDefault(); 2370 e.stopPropagation(); 2371 placeidHelpModal.style.display = 'flex'; 2372 }); 2373 2374 placeidHelpClose.addEventListener('click', function() { 2375 placeidHelpModal.style.display = 'none'; 2376 }); 2377 2378 placeidHelpCloseBtn.addEventListener('click', function() { 2379 placeidHelpModal.style.display = 'none'; 2380 }); 2381 2382 // Close on backdrop click 2383 placeidHelpModal.addEventListener('click', function(e) { 2384 if (e.target === placeidHelpModal) { 2385 placeidHelpModal.style.display = 'none'; 2386 } 2387 }); 2388 2389 // Close on Escape key 2390 document.addEventListener('keydown', function(e) { 2391 if (e.key === 'Escape' && placeidHelpModal.style.display === 'flex') { 2392 placeidHelpModal.style.display = 'none'; 2393 } 2394 }); 2395 } 2396 2397 // Google Places Info Modal 2398 const googlePlacesModal = document.getElementById('google-places-modal'); 2399 const googlePlacesClose = document.getElementById('google-places-close'); 2400 const googlePlacesCloseBtn = document.getElementById('google-places-close-btn'); 2401 2402 if (googlePlacesModal) { 2403 if (googlePlacesClose) { 2404 googlePlacesClose.addEventListener('click', function() { 2405 googlePlacesModal.style.display = 'none'; 2406 }); 2407 } 2408 2409 if (googlePlacesCloseBtn) { 2410 googlePlacesCloseBtn.addEventListener('click', function() { 2411 googlePlacesModal.style.display = 'none'; 2412 }); 2413 } 2414 2415 // Close on backdrop click 2416 googlePlacesModal.addEventListener('click', function(e) { 2417 if (e.target === googlePlacesModal) { 2418 googlePlacesModal.style.display = 'none'; 2419 } 2420 }); 2421 2422 // Close on Escape key 2423 document.addEventListener('keydown', function(e) { 2424 if (e.key === 'Escape' && googlePlacesModal.style.display === 'flex') { 2425 googlePlacesModal.style.display = 'none'; 2426 } 2427 }); 2428 2429 // Info link to show modal on demand 2430 const showGooglePlacesInfo = document.getElementById('show-google-places-info'); 2431 if (showGooglePlacesInfo) { 2432 showGooglePlacesInfo.addEventListener('click', function() { 2433 googlePlacesModal.style.display = 'flex'; 2434 }); 2435 } 2436 } 2437 2438 // Review tab input handlers 2439 const reviewPlaceidInput = document.getElementById('review-placeid'); 2440 const reviewBusinessInput = document.getElementById('review-business'); 2441 const reviewUrlPreview = document.getElementById('review-url-preview'); 2442 2443 function updateReviewUrlPreview() { 2444 const placeid = reviewPlaceidInput ? reviewPlaceidInput.value.trim() : ''; 2445 2446 if (placeid && placeid.startsWith('ChIJ')) { 2447 const reviewUrl = 'https://search.google.com/local/writereview?placeid=' + placeid; 2448 reviewUrlPreview.textContent = reviewUrl;
2449 reviewUrlPreview.classList.add('valid'); 2450 } else if (placeid) { 2451 reviewUrlPreview.textContent = 'Place ID should start with "ChIJ"'; 2452 reviewUrlPreview.classList.remove('valid'); 2453 } else { 2454 reviewUrlPreview.textContent = 'Enter Place ID to generate review link'; 2455 reviewUrlPreview.classList.remove('valid'); 2456 } 2457 } 2458 2459 if (reviewPlaceidInput) { 2460 reviewPlaceidInput.addEventListener('input', function() { 2461 updateReviewUrlPreview(); 2462 if (typeof updateQRFromActiveTab === 'function') { 2463 clearTimeout(window.reviewDebounce); 2464 window.reviewDebounce = setTimeout(updateQRFromActiveTab, 300); 2465 } 2466 }); 2467 } 2468 2469 if (reviewBusinessInput) { 2470 reviewBusinessInput.addEventListener('input', function() { 2471 // Business name is optional, just for reference 2472 // Could be used later for display purposes 2473 }); 2474 } 2475 2476 // Update preview info when size or format changes 2477 function updatePreviewInfo() { 2478 const size = document.getElementById('qr-size').value || 77; 2479 const format = document.querySelector('.format-btn.active').dataset.format.toUpperCase(); 2480 const shape = document.querySelector('.shape-btn.active').dataset.shape; 2481 2482 let dims = size + 'mm à ' + size + 'mm'; 2483 if (shape === 'house') { 2484 // Blocky house has aspect ratio based on 109.91mm x 105.43mm viewBox 2485 const height = Math.round(size * (105.43 / 109.91)); 2486 dims = size + 'mm à ' + height + 'mm'; 2487 } else if (shape === 'houseA' || shape === 'houseB') { 2488 // House shapes have aspect ratio based on 114.7mm x 125.4mm viewBox 2489 const height = Math.round(size * (125.4 / 114.7)); 2490 dims = size + 'mm à ' + height + 'mm'; 2491 } 2492 2493 document.getElementById('preview-info').textContent = dims + ' ⢠' + format; 2494 } 2495 2496 document.getElementById('qr-size').addEventListener('input', updatePreviewInfo); 2497 document.querySelectorAll('.format-btn').forEach(btn => { 2498 btn.addEventListener('click', updatePreviewInfo); 2499 }); 2500 document.querySelectorAll('.shape-btn').forEach(btn => { 2501 btn.addEventListener('click', function() { 2502 setTimeout(updatePreviewInfo, 50); 2503 }); 2504 }); 2505 2506}); 2507</script>
2507 2508 2509 2510 <!-- Scripts at end of body for faster page render --> 2511 <!-- Fabric.js - loaded async from LaserLab CDN --> 2512
2512<script> 2513 // Load Fabric.js asynchronously 2514 (function() { 2515 var script = document.createElement('script'); 2516 script.src = 'https://assets.laserlab.app/fabric.min.js'; 2517 script.async = true; 2518 script.crossOrigin = 'anonymous'; 2519 script.onload = function() { 2520 console.log('â Fabric.js loaded'); 2521 // Dispatch event so imageprep.js knows Fabric is ready 2522 window.dispatchEvent(new Event('fabricReady')); 2523 }; 2524 script.onerror = function() { 2525 console.error('â Fabric.js failed from R2, trying cdnjs fallback'); 2526 var fallback = document.createElement('script'); 2527 fallback.src = 'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js'; 2528 fallback.async = true; 2529 fallback.onload = function() { 2530 console.log('â Fabric.js loaded (cdnjs fallback)'); 2531 window.dispatchEvent(new Event('fabricReady')); 2532 }; 2533 document.head.appendChild(fallback); 2534 }; 2535 document.head.appendChild(script); 2536 })(); 2537 </script>
2537 2538 2539 <!-- noUiSlider - small, load normally --> 2540
2540<script src="https://assets.laserlab.app/nouislider.min.js"></script>
2540 2541 2542 <!-- OpenCV.js - Preloaded in background after page loads --> 2543
2543<script> 2544 window.cvReady = false; 2545 let cvReady = false; 2546 let cvLoading = false; 2547 2548 function loadOpenCV(callback) { 2549 // Already loaded 2550 if (cvReady) { 2551 if (callback) callback(); 2552 return; 2553 } 2554 2555 // Already loading - queue callback 2556 if (cvLoading) { 2557 const checkReady = setInterval(() => { 2558 if (cvReady) { 2559 clearInterval(checkReady); 2560 if (callback) callback(); 2561 } 2562 }, 100); 2563 return; 2564 } 2565 2566 // Start loading 2567 cvLoading = true; 2568 console.log('â³ Loading OpenCV.js (full build with inpaint)...'); 2569 2570 const script = document.createElement('script'); 2571 // Primary: LaserLab Cloudflare R2 bucket (fast, reliable, brotli compressed) 2572 script.src = 'https://assets.laserlab.app/opencv.js'; 2573 script.async = true; 2574 script.crossOrigin = 'anonymous'; 2575 script.onload = function() { 2576 console.log('OpenCV script loaded, waiting for runtime...'); 2577 if (typeof cv !== 'undefined') { 2578 if (typeof cv.onRuntimeInitialized === 'function' || cv.onRuntimeInitialized === undefined) { 2579 cv['onRuntimeInitialized'] = () => { 2580 cvReady = true; 2581 window.cvReady = true; 2582 cvLoading = false; 2583 console.log('â OpenCV.js READY - cv.inpaint available:', typeof cv.inpaint === 'function'); 2584 if (callback) callback(); 2585 }; 2586 } else { 2587 // Already initialized 2588 cvReady = true; 2589 window.cvReady = true; 2590 cvLoading = false; 2591 console.log('â OpenCV.js READY (immediate) - cv.inpaint available:', typeof cv.inpaint === 'function'); 2592 if (callback) callback(); 2593 } 2594 } 2595 }; 2596 script.onerror = function() { 2597 cvLoading = false; 2598 console.error('â Failed to load OpenCV.js from assets.laserlab.app'); 2599 // Fallback 1: docs.opencv.org 2600 console.log('Trying docs.opencv.org fallback...'); 2601 const fallback1 = document.createElement('script'); 2602 fallback1.src = 'https://docs.opencv.org/4.x/opencv.js'; 2603 fallback1.async = true; 2604 fallback1.onload = function() { 2605 if (typeof cv !== 'undefined') { 2606 cv['onRuntimeInitialized'] = () => { 2607 cvReady = true; 2608 window.cvReady = true; 2609 cvLoading = false; 2610 console.log('â OpenCV.js READY (opencv.org fallback) - cv.inpaint available:', typeof cv.inpaint === 'function'); 2611 if (callback) callback(); 2612 }; 2613 } 2614 }; 2615 fallback1.onerror = function() { 2616 // Fallback 2: jsdelivr (may not have inpaint) 2617 console.log('Trying jsdelivr fallback (may not have inpaint)...'); 2618 const fallback2 = document.createElement('script'); 2619 fallback2.src = 'https://cdn.jsdelivr.net/npm/[email protected]/opencv.min.js'; 2620 fallback2.async = true; 2621 fallback2.onload = function() { 2622 if (typeof cv !== 'undefined') { 2623 cv['onRuntimeInitialized'] = () => { 2624 cvReady = true; 2625 window.cvReady = true; 2626 console.log('â OpenCV.js READY (jsdelivr fallback) - cv.inpaint available:', typeof cv.inpaint === 'function'); 2627 if (callback) callback(); 2628 }; 2629 } 2630 }; 2631 document.body.appendChild(fallback2); 2632 }; 2633 document.body.appendChild(fallback1); 2634 }; 2635 document.body.appendChild(script); 2636 } 2637 2638 // Preload OpenCV in background - ONLY on ImagePrep page 2639 // Slicer doesn't need OpenCV and loading it slows down the page 2640 window.addEventListener('load', function() { 2641 const isImagePrep = window.location.pathname.includes('imageprep'); 2642 2643 if (isImagePrep) { 2644 // Load immediately for imageprep (needed for spot removal, auto-enhance) 2645 setTimeout(function() { 2646 if (!cvReady && !cvLoading) { 2647 console.log('ð Preloading OpenCV.js for ImagePrep...'); 2648 loadOpenCV(); 2649 } 2650 }, 100); 2651 } 2652 // Don't preload on other pages - they don't need it 2653 }); 2654 </script>
2654 2655 2656 2657 2658 <!-- Service Worker Registration (caches big libraries for faster repeat visits) --> 2659
2659<script> 2660 if ('serviceWorker' in navigator) { 2661 window.addEventListener('load', function() { 2662 navigator.serviceWorker.register('/sw.js') 2663 .then(function(registration) { 2664 console.log('â Service Worker registered'); 2665 }) 2666 .catch(function(error) { 2667 console.log('Service Worker registration failed:', error); 2668 }); 2669 }); 2670 } 2671 </script>
vendor: 1 bytes, line 2671
2671
2672<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":"5428fc1abd69496794171f0c809c326a","r":1,"spa":2}' crossorigin="anonymous"></script>
2672 2673</body> 2674</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.