Templates.purs (20244B)
1 module Templates where 2 3 import Prelude 4 import Data.String.Common (joinWith) 5 6 indexHtml :: String -> Array { slug :: String, name :: String } -> String 7 indexHtml userSlug allUsers = 8 let 9 encodeUser { slug, name } = "{\"slug\":\"" <> slug <> "\",\"name\":\"" <> name <> "\"}" 10 usersJson = "[" <> joinWith "," (map encodeUser allUsers) <> "]" 11 in 12 """<!DOCTYPE html> 13 <html lang="en"> 14 <head> 15 <meta charset="UTF-8"> 16 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 17 <title>scrobbler</title> 18 <link rel="icon" type="image/png" href="/favicon.png"> 19 <link rel="preconnect" href="https://fonts.googleapis.com"> 20 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 21 <link href="https://fonts.googleapis.com/css2?family=Intel+One+Mono:wght@300;400;500;700&display=swap" rel="stylesheet"> 22 <style> 23 body { 24 font-family: 'Intel One Mono', 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 25 background: oklch(0.194 0.0345 225.31); 26 color: oklch(0.91 0.012 225.31); 27 margin: 0; 28 line-height: 1.6; 29 } 30 31 ::selection { 32 background: oklch(0.42 0.04 225.31); 33 color: oklch(0.91 0.012 225.31); 34 } 35 36 .container { 37 max-width: 800px; 38 margin: 0 auto; 39 padding: 24px 20px; 40 } 41 42 h1 { 43 color: oklch(0.91 0.012 225.31); 44 margin: 0 0 20px 0; 45 font-size: 24px; 46 border-bottom: 2px solid oklch(0.42 0.04 225.31); 47 padding-bottom: 8px; 48 } 49 50 h1.site-header { 51 border-bottom: none; 52 padding-bottom: 0; 53 line-height: 0; 54 cursor: pointer; 55 } 56 57 h1.site-header img { 58 display: block; 59 width: 100%; 60 height: 80px; 61 object-fit: cover; 62 border-radius: 4px; 63 border: 1px solid oklch(0.42 0.04 225.31); 64 } 65 66 ul { 67 list-style: none; 68 padding: 0; 69 margin: 0 0 20px 0; 70 } 71 72 li { 73 background: oklch(0.27 0.038 225.31); 74 border: 1px solid oklch(0.42 0.04 225.31); 75 border-radius: 4px; 76 padding: 5px 15px 5px 15px; 77 margin-bottom: 5px; 78 display: flex; 79 justify-content: space-between; 80 align-items: center; 81 box-shadow: 4px 4px 0px oklch(0.42 0.04 225.31); 82 } 83 84 li.success { 85 background: oklch(0.27 0.038 225.31); 86 border-color: oklch(0.42 0.04 225.31); 87 box-shadow: 4px 4px 0px oklch(0.42 0.04 225.31); 88 } 89 90 .track-info { 91 flex: 1; 92 } 93 94 .track-name { 95 font-weight: bold; 96 font-size: 16px; 97 color: oklch(0.91 0.012 225.31); 98 } 99 100 .track-artist { 101 font-size: 14px; 102 color: oklch(0.70 0.035 225.31); 103 margin-top: 1px; 104 } 105 106 .track-time { 107 font-size: 12px; 108 color: oklch(0.74 0.075 225.31); 109 margin-top: 2px; 110 } 111 112 .track-link { 113 background: none; 114 border: none; 115 padding: 0; 116 color: oklch(0.91 0.012 225.31); 117 font-weight: bold; 118 font-size: 16px; 119 font-family: inherit; 120 cursor: pointer; 121 text-align: left; 122 user-select: text; 123 } 124 125 .artist-link { 126 background: none; 127 border: none; 128 padding: 0; 129 color: oklch(0.70 0.035 225.31); 130 font-size: 14px; 131 font-family: inherit; 132 cursor: pointer; 133 text-align: left; 134 user-select: text; 135 } 136 137 .album-link { 138 background: none; 139 border: none; 140 padding: 0; 141 color: oklch(0.74 0.075 225.31); 142 text-decoration: underline; 143 font-family: inherit; 144 font-size: inherit; 145 cursor: pointer; 146 } 147 148 .album-link:hover { 149 color: oklch(0.78 0.13 357.86); 150 } 151 152 .label-link { 153 background: none; 154 border: none; 155 padding: 0; 156 color: oklch(0.74 0.075 225.31); 157 text-decoration: underline; 158 font-family: inherit; 159 font-size: inherit; 160 cursor: pointer; 161 } 162 163 .label-link:hover { 164 color: oklch(0.78 0.13 357.86); 165 } 166 167 .track-cover { 168 width: 60px; 169 height: 60px; 170 border-radius: 4px; 171 object-fit: cover; 172 background: rgba(255, 255, 255, 0.05); 173 transition: transform 0.2s ease-in-out; 174 cursor: pointer; 175 } 176 177 .track-cover.zoomed { 178 transform: scale(5.0); 179 z-index: 10; 180 position: relative; 181 box-shadow: 0 8px 16px rgba(0, 0, 0, 0.5); 182 } 183 184 .loading { 185 padding: 20px; 186 color: oklch(0.74 0.075 225.31); 187 text-align: center; 188 } 189 190 .error { 191 padding: 20px; 192 color: oklch(0.78 0.085 225.31); 193 text-align: center; 194 } 195 196 .small { 197 font-size: 12px; 198 color: oklch(0.74 0.075 225.31); 199 margin-top: 20px; 200 } 201 202 .small a { 203 color: oklch(0.70 0.035 225.31); 204 text-decoration: none; 205 } 206 207 .small a:hover { 208 color: oklch(0.78 0.13 357.86); 209 text-decoration: underline; 210 } 211 212 .pagination { 213 display: flex; 214 justify-content: center; 215 gap: 20px; 216 margin-top: 20px; 217 } 218 219 .page-btn { 220 background: oklch(0.27 0.038 225.31); 221 border: 1px solid oklch(0.42 0.04 225.31); 222 color: oklch(0.91 0.012 225.31); 223 padding: 8px 16px; 224 border-radius: 4px; 225 cursor: pointer; 226 font-family: inherit; 227 box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31); 228 } 229 230 .page-btn:hover { 231 background: oklch(0.32 0.045 225.31); 232 border-color: oklch(0.52 0.05 225.31); 233 } 234 235 .page-btn:disabled { 236 opacity: 0.5; 237 cursor: not-allowed; 238 } 239 240 .page-indicator { 241 display: flex; 242 align-items: center; 243 font-size: 14px; 244 color: oklch(0.74 0.075 225.31); 245 } 246 247 .tabs { 248 display: flex; 249 gap: 10px; 250 margin-bottom: 20px; 251 } 252 253 .tab-btn { 254 background: none; 255 border: 1px solid oklch(0.42 0.04 225.31); 256 color: oklch(0.74 0.075 225.31); 257 padding: 6px 14px; 258 border-radius: 4px; 259 cursor: pointer; 260 font-family: inherit; 261 font-size: 12px; 262 text-decoration: none; 263 display: inline-block; 264 } 265 266 .tab-btn.active { 267 background: oklch(0.27 0.038 225.31); 268 color: oklch(0.91 0.012 225.31); 269 box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31); 270 } 271 272 .tab-btn:hover { 273 color: oklch(0.91 0.012 225.31); 274 background: oklch(0.42 0.04 225.31); 275 } 276 277 .stats-section { 278 margin-bottom: 30px; 279 } 280 281 .stats-section h2 { 282 font-size: 11px; 283 color: oklch(0.74 0.075 225.31); 284 text-transform: uppercase; 285 letter-spacing: 3px; 286 margin: 0 0 10px 0; 287 border-bottom: 1px solid oklch(0.42 0.04 225.31); 288 padding-bottom: 5px; 289 } 290 291 .stat-row { 292 display: flex; 293 justify-content: space-between; 294 align-items: center; 295 padding: 5px 8px; 296 margin-bottom: 3px; 297 position: relative; 298 border-radius: 2px; 299 overflow: hidden; 300 font-size: 13px; 301 } 302 303 .stat-bar { 304 position: absolute; 305 left: 0; 306 top: 0; 307 height: 100%; 308 background: oklch(0.27 0.038 225.31); 309 border-right: 1px solid oklch(0.42 0.04 225.31); 310 z-index: 0; 311 } 312 313 .stat-name { 314 position: relative; 315 z-index: 1; 316 color: oklch(0.91 0.012 225.31); 317 flex: 1; 318 padding-right: 10px; 319 white-space: nowrap; 320 overflow: hidden; 321 text-overflow: ellipsis; 322 } 323 324 .stat-count { 325 position: relative; 326 z-index: 1; 327 color: oklch(0.74 0.075 225.31); 328 font-size: 12px; 329 flex-shrink: 0; 330 } 331 332 .stats-empty { 333 color: oklch(0.74 0.075 225.31); 334 font-size: 13px; 335 padding: 10px 0; 336 } 337 338 .cover-wrapper { 339 display: flex; 340 flex-direction: column; 341 align-items: center; 342 padding-top: 5px; 343 margin-left: 15px; 344 gap: 4px; 345 flex-shrink: 0; 346 position: relative; 347 } 348 349 .genre-tag { 350 display: none; 351 } 352 353 354 355 .stat-row.clickable { 356 cursor: pointer; 357 } 358 359 .stat-row.clickable:hover .stat-name { 360 color: oklch(0.70 0.035 225.31); 361 } 362 363 .filter-banner { 364 display: flex; 365 align-items: center; 366 gap: 10px; 367 background: oklch(0.27 0.038 225.31); 368 border: 1px solid oklch(0.42 0.04 225.31); 369 border-radius: 4px; 370 padding: 8px 12px; 371 margin-bottom: 12px; 372 font-size: 13px; 373 color: oklch(0.74 0.075 225.31); 374 } 375 376 .filter-label { 377 flex: 1; 378 } 379 380 .filter-label strong { 381 color: oklch(0.91 0.012 225.31); 382 } 383 384 .filter-clear { 385 background: none; 386 border: 1px solid oklch(0.42 0.04 225.31); 387 color: oklch(0.74 0.075 225.31); 388 padding: 2px 8px; 389 border-radius: 3px; 390 cursor: pointer; 391 font-family: inherit; 392 font-size: 12px; 393 } 394 395 .filter-clear:hover { 396 color: oklch(0.91 0.012 225.31); 397 border-color: oklch(0.52 0.05 225.31); 398 background: oklch(0.32 0.045 225.31); 399 } 400 401 .show-all-btn { 402 background: none; 403 border: none; 404 color: oklch(0.74 0.075 225.31); 405 cursor: pointer; 406 font-family: inherit; 407 font-size: 12px; 408 padding: 4px 0; 409 text-decoration: underline; 410 } 411 412 .show-all-btn:hover { 413 color: oklch(0.91 0.012 225.31); 414 background: oklch(0.32 0.045 225.31); 415 } 416 417 .period-selector { 418 display: flex; 419 gap: 6px; 420 margin-bottom: 16px; 421 } 422 423 .period-btn { 424 background: none; 425 border: 1px solid oklch(0.42 0.04 225.31); 426 color: oklch(0.74 0.075 225.31); 427 padding: 4px 12px; 428 border-radius: 3px; 429 cursor: pointer; 430 font-family: inherit; 431 font-size: 12px; 432 } 433 434 .period-btn:hover { 435 color: oklch(0.91 0.012 225.31); 436 border-color: oklch(0.7 0.1274 357.86); 437 } 438 439 .period-btn.active { 440 background: oklch(0.5 0.1274 357.86); 441 border-color: oklch(0.7 0.1274 357.86); 442 color: oklch(0.97 0.01 357.86); 443 } 444 445 .custom-range { 446 display: flex; 447 gap: 8px; 448 margin-top: 8px; 449 align-items: center; 450 } 451 452 .custom-range-input { 453 background: oklch(0.27 0.038 225.31); 454 border: 1px solid oklch(0.42 0.04 225.31); 455 color: oklch(0.91 0.012 225.31); 456 padding: 4px 8px; 457 border-radius: 3px; 458 font-family: inherit; 459 font-size: 12px; 460 width: 220px; 461 } 462 463 .custom-range-input::placeholder { 464 color: oklch(0.74 0.075 225.31); 465 opacity: 0.7; 466 } 467 468 .custom-range-input:focus { 469 outline: none; 470 border-color: oklch(0.91 0.012 225.31); 471 } 472 473 .custom-range-input.error { 474 border-color: oklch(0.68 0.11 225.31); 475 } 476 477 .custom-range-error { 478 color: oklch(0.68 0.11 225.31); 479 font-size: 12px; 480 margin-top: 4px; 481 } 482 483 .similar-btn { 484 background: none; 485 border: 1px solid oklch(0.42 0.04 225.31); 486 color: oklch(0.74 0.075 225.31); 487 padding: 2px 8px; 488 border-radius: 3px; 489 cursor: pointer; 490 font-family: inherit; 491 font-size: 10px; 492 margin-top: 4px; 493 transition: all 0.2s ease; 494 } 495 496 .similar-btn:hover { 497 background: oklch(0.32 0.045 225.31); 498 color: oklch(0.91 0.012 225.31); 499 border-color: oklch(0.7 0.1274 357.86); 500 } 501 502 .similar-btn.active { 503 background: oklch(0.5 0.1274 357.86); 504 color: oklch(0.97 0.01 357.86); 505 border-color: oklch(0.7 0.1274 357.86); 506 box-shadow: 2px 2px 0px oklch(0.7 0.1274 357.86); 507 } 508 509 .similar-panel { 510 margin-top: 10px; 511 background: oklch(0.27 0.038 225.31); 512 border: 1px solid oklch(0.42 0.04 225.31); 513 border-radius: 4px; 514 padding: 10px; 515 box-shadow: 2px 2px 0px oklch(0.42 0.04 225.31); 516 } 517 518 .similar-panel-header { 519 font-size: 11px; 520 color: oklch(0.74 0.075 225.31); 521 text-transform: uppercase; 522 letter-spacing: 1px; 523 margin-bottom: 8px; 524 font-weight: bold; 525 } 526 527 .similar-loading { 528 color: oklch(0.74 0.075 225.31); 529 font-size: 12px; 530 text-align: center; 531 padding: 8px; 532 } 533 534 .similar-error { 535 color: oklch(0.78 0.085 225.31); 536 font-size: 12px; 537 text-align: center; 538 padding: 8px; 539 } 540 541 .similar-empty { 542 color: oklch(0.74 0.075 225.31); 543 font-size: 12px; 544 text-align: center; 545 padding: 8px; 546 } 547 548 .similar-track { 549 display: flex; 550 justify-content: space-between; 551 align-items: center; 552 padding: 6px 8px; 553 margin-bottom: 4px; 554 background: oklch(0.27 0.038 225.31); 555 border-radius: 3px; 556 border-left: 2px solid oklch(0.42 0.04 225.31); 557 } 558 559 .similar-track:last-child { 560 margin-bottom: 0; 561 } 562 563 .similar-track-info { 564 flex: 1; 565 min-width: 0; 566 } 567 568 .similar-track-name { 569 font-size: 12px; 570 color: oklch(0.91 0.012 225.31); 571 font-weight: 500; 572 white-space: nowrap; 573 overflow: hidden; 574 text-overflow: ellipsis; 575 } 576 577 .similar-track-artist { 578 font-size: 11px; 579 color: oklch(0.70 0.035 225.31); 580 margin-top: 1px; 581 white-space: nowrap; 582 overflow: hidden; 583 text-overflow: ellipsis; 584 } 585 586 .similar-score { 587 font-size: 10px; 588 color: oklch(0.74 0.075 225.31); 589 background: oklch(0.27 0.038 225.31); 590 border: 1px solid oklch(0.42 0.04 225.31); 591 padding: 2px 6px; 592 border-radius: 2px; 593 margin-right: 8px; 594 font-weight: 500; 595 } 596 597 .similar-link { 598 color: oklch(0.74 0.075 225.31); 599 text-decoration: none; 600 font-size: 12px; 601 background: oklch(0.27 0.038 225.31); 602 border: 1px solid oklch(0.42 0.04 225.31); 603 padding: 2px 6px; 604 border-radius: 2px; 605 transition: all 0.2s ease; 606 } 607 608 .similar-link:hover { 609 color: oklch(0.78 0.13 357.86); 610 background: oklch(0.42 0.04 225.31); 611 } 612 613 .tracks-with-similar { 614 display: flex; 615 flex-direction: column; 616 gap: 10px; 617 } 618 619 .track-with-similar-container { 620 display: flex; 621 flex-direction: column; 622 gap: 10px; 623 } 624 625 .track-item { 626 list-style: none; 627 padding: 0; 628 margin: 0; 629 } 630 631 .track-item li { 632 margin: 0; 633 } 634 635 .search-bar { 636 display: flex; 637 gap: 8px; 638 margin-bottom: 12px; 639 align-items: center; 640 } 641 642 .search-input { 643 flex: 1; 644 background: oklch(0.27 0.038 225.31); 645 border: 1px solid oklch(0.42 0.04 225.31); 646 color: oklch(0.91 0.012 225.31); 647 padding: 6px 10px; 648 border-radius: 4px; 649 font-family: inherit; 650 font-size: 13px; 651 } 652 653 .search-input::placeholder { 654 color: oklch(0.74 0.075 225.31); 655 opacity: 0.7; 656 } 657 658 .search-input:focus { 659 outline: none; 660 border-color: oklch(0.91 0.012 225.31); 661 } 662 663 .search-btn { 664 background: oklch(0.32 0.045 225.31); 665 border: 1px solid oklch(0.52 0.05 225.31); 666 color: oklch(0.91 0.012 225.31); 667 padding: 6px 12px; 668 border-radius: 4px; 669 cursor: pointer; 670 font-family: inherit; 671 font-size: 12px; 672 } 673 674 .search-btn:hover { 675 background: oklch(0.42 0.05 225.31); 676 border-color: oklch(0.52 0.05 225.31); 677 } 678 679 .search-bar:focus-within .search-btn { 680 background: oklch(0.5 0.1274 357.86); 681 border-color: oklch(0.7 0.1274 357.86); 682 color: oklch(0.97 0.01 357.86); 683 } 684 685 .about-lead { 686 color: oklch(0.70 0.035 225.31); 687 font-size: 13px; 688 line-height: 1.8; 689 margin: 0 0 30px 0; 690 } 691 692 .about-link { 693 color: oklch(0.74 0.075 225.31); 694 text-decoration: underline; 695 } 696 697 .about-link:hover { 698 color: oklch(0.78 0.13 357.86); 699 } 700 701 .about-list { 702 list-style: none; 703 padding: 0; 704 margin: 0; 705 } 706 707 .about-list li { 708 background: none; 709 border: none; 710 border-radius: 0; 711 padding: 3px 0; 712 margin-bottom: 0; 713 display: block; 714 box-shadow: none; 715 font-size: 13px; 716 color: oklch(0.70 0.035 225.31); 717 } 718 719 .about-list li::before { 720 content: "→ "; 721 color: oklch(0.42 0.04 225.31); 722 } 723 724 .about-meta { 725 font-size: 13px; 726 margin: 0; 727 } 728 729 .about-users { 730 list-style: none; 731 padding: 0; 732 margin: 0; 733 } 734 735 .about-users li { 736 background: none; 737 border: none; 738 border-radius: 0; 739 padding: 4px 0; 740 margin-bottom: 0; 741 display: block; 742 box-shadow: none; 743 } 744 745 .about-users a { 746 color: oklch(0.74 0.075 225.31); 747 text-decoration: none; 748 font-size: 13px; 749 } 750 751 .about-users a:hover { 752 color: oklch(0.78 0.13 357.86); 753 text-decoration: underline; 754 } 755 </style> 756 </head> 757 <body> 758 <div id="app"></div> 759 <script src="/client.js"></script> 760 <script> 761 var userSlug = '""" <> userSlug 762 <> 763 """'; 764 var allUsers = """ 765 <> usersJson 766 <> 767 """; 768 var app = Elm.Client.init({ 769 flags: { userSlug: userSlug, allUsers: allUsers } 770 }); 771 </script> 772 </body> 773 </html>"""