corpus

self-hosted listenbrainz and last.fm proxy
Log | Files | Refs | README | LICENSE

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>"""