Răsfoiți Sursa

Website redesign

Clément Wolf 1 an în urmă
părinte
comite
52e4fc04be

+ 2 - 0
.gitignore

@@ -4,6 +4,8 @@ node_modules/
 
 config.json
 
+*.min.css
+*.min.js
 *.db
 *.db-shm
 *.db-wal

+ 3 - 3
commands.js

@@ -169,14 +169,14 @@ async function wait_command(msg, match, lobby) {
 async function about_command(msg, match, lobby) {
   if (lobby) {
     if (lobby.data.type == 'collection') {
-      await lobby.send(`This lobby will auto-select maps of a specific collection from osu!collector. All commands and answers to your questions are [${Config.discord_invite_link} in the Discord.]`);
+      await lobby.send(`This lobby will auto-select maps of a specific collection from osu!collector. More info: ${website_base_url}/faq/`);
     } else if (lobby.data.type == 'ranked') {
-      await lobby.send(`In this lobby, you get a rank based on how often you pass maps with 95% accuracy. All commands and answers to your questions are [${Config.discord_invite_link} in the Discord.]`);
+      await lobby.send(`In this lobby, you get a rank based on how often you pass maps with 95% accuracy. More info: ${website_base_url}/faq/`);
     } else {
       await lobby.send(`Bruh just send !collection <id> or !ranked <ruleset>`);
     }
   } else {
-    await bancho.privmsg(msg.from, `This bot can join lobbies and do many things. Commands and answers to your questions are available [${Config.discord_invite_link} in the Discord.]`);
+    await bancho.privmsg(msg.from, `${website_base_url}/faq/`);
   }
 }
 

+ 1 - 45
glicko.js

@@ -30,23 +30,6 @@ const RANK_DIVISIONS = [
   'Rhythm Incarnate',
 ];
 
-const RANK_DIVISIONS_COLORS = [
-  '#ad8762',
-  '#966F33',
-  '#966F33',
-  '#CD7F32',
-  '#CD7F32',
-  '#C0C0C0',
-  '#C0C0C0',
-  '#FFD700',
-  '#FFD700',
-  '#E5E4E2',
-  '#E5E4E2',
-  '#b9f2ff',
-  '#b9f2ff',
-  '#ff1a1a;',
-];
-
 
 // TODO: move to postgresql before deploy?
 
@@ -330,31 +313,6 @@ function get_rank_text(rank_float, nb_scores) {
   return RANK_DIVISIONS[RANK_DIVISIONS.length - 1];
 }
 
-function get_rank_color(rank_float, nb_scores) {
-  if (!rank_float || nb_scores < 5) {
-    return 'Unranked';
-  }
-  if (rank_float == 1.0) {
-    return 'The One';
-  }
-
-  // Epic rank distribution algorithm
-  for (let i = 0; i < RANK_DIVISIONS_COLORS.length; i++) {
-    // Turn current 'Cardboard' rank into a value between 0 and 1
-    const rank_nb = (i + 1) / RANK_DIVISIONS_COLORS.length;
-
-    // To make climbing ranks more satisfying, we make lower ranks more common.
-    // Visual representation: https://graphtoy.com/?f1(x,t)=1-((cos(x%5E0.8*%F0%9D%9C%8B)/2)+0.5)&v1=true&f2(x,t)=&v2=true&f3(x,t)=&v3=false&f4(x,t)=&v4=false&f5(x,t)=&v5=false&f6(x,t)=&v6=false&grid=true&coords=0.3918011117299855,0.3722110561434862,1.0068654346588846
-    const cutoff = 1 - ((Math.cos(Math.pow(rank_nb, 0.8) * Math.PI) / 2) + 0.5);
-    if (rank_float < cutoff) {
-      return RANK_DIVISIONS_COLORS[i];
-    }
-  }
-
-  // Ok, floating point errors, who cares
-  return RANK_DIVISIONS_COLORS[RANK_DIVISIONS_COLORS.length - 1];
-}
-
 
 function get_map_rank(map_id) {
   const res = db.prepare(`
@@ -416,8 +374,7 @@ function get_user_ranks(user_id) {
   `).get(elos.osu_elo, elos.taiko_elo, elos.catch_elo, elos.mania_elo);
 
   const build_rating = (mode, elo, nb_total, nb_better, nb_scores) => {
-    const ratio = 0.9999;
-    nb_scores = 8;
+    const ratio = 1.0 - (nb_better / nb_total);
     return {
       mode: mode,
       elo: nb_scores < 5 ? '???' : elo,
@@ -426,7 +383,6 @@ function get_user_ranks(user_id) {
       rank_nb: nb_scores < 5 ? '???' : nb_better + 1,
       nb_scores: nb_scores,
       text: get_rank_text(ratio, nb_scores),
-      rank_cr: get_rank_color(ratio, nb_scores),
     };
   };
   return [

+ 5 - 2
package.json

@@ -6,7 +6,9 @@
   "license": "AGPL-3.0-only",
   "type": "module",
   "scripts": {
-    "start": "node index.js"
+    "start": "node index.js",
+    "build": "npx tailwindcss -i ./public/stylesheet.css -o ./public/styles.min.css --minify",
+    "watch": "npx tailwindcss -i ./public/stylesheet.css -o ./public/styles.min.css --watch"
   },
   "dependencies": {
     "@discordjs/builders": "^0.6.0",
@@ -27,6 +29,7 @@
   },
   "devDependencies": {
     "eslint": "^7.32.0",
-    "eslint-config-google": "^0.14.0"
+    "eslint-config-google": "^0.14.0",
+    "tailwindcss": "^3.2.1"
   }
 }

BIN
public/favicon-16x16.png


BIN
public/favicon-32x32.png


BIN
public/favicon-96x96.png


BIN
public/favicon.ico


BIN
public/images/discord.png


BIN
public/images/github.png


BIN
public/images/leaderboard.png


BIN
public/images/login.png


BIN
public/images/o!RL-logo.png


+ 233 - 263
public/index.html

@@ -4,339 +4,309 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>osu! ranked lobbies</title>
-    <link rel="stylesheet" href="/reset.css?v=1.0">
-    <link rel="stylesheet" href="/stylesheet.css?v=2.1">
+    <link rel="stylesheet" href="/styles.min.css?v=1.0">
     <link rel="stylesheet" href="/fa-main.min.css">
     <link rel="stylesheet" href="/fa-solid.min.css">
     <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
     <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
     <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
   </head>
-  <body>
-    <div class="search-button">
-      <svg class="search-icon" xmlns="http://www.w3.org/2000/svg"
-        width="24" height="24"
-        viewBox="0 0 24 24"><path d="M 9 2 C 5.1458514 2 2 5.1458514 2 9 C 2 12.854149 5.1458514 16 9 16 C 10.747998 16 12.345009 15.348024 13.574219 14.28125 L 14 14.707031 L 14 16 L 20 22 L 22 20 L 16 14 L 14.707031 14 L 14.28125 13.574219 C 15.348024 12.345009 16 10.747998 16 9 C 16 5.1458514 12.854149 2 9 2 z M 9 4 C 11.773268 4 14 6.2267316 14 9 C 14 11.773268 11.773268 14 9 14 C 6.2267316 14 4 11.773268 4 9 C 4 6.2267316 6.2267316 4 9 4 z"></path>
-      </svg>
-      <input type="text" placeholder="&nbsp;">
-      <div class="search-results"></div>
-    </div>
-    <div class="search-background"></div>
-
-    <div class="sidebar">
-      <a class="logo" href="/lobbies/">
-        <img src="/images/o!RL-logo.png" />
-      </a>
-      <nav>
-        <div class="other-links">
-          <a class="login_link" href="/osu_login">
-            <img src="/images/login.png" />
-            <span>Login</span>
+  <body class="bg-zinc-800">
+    <header class="h-16 bg-zinc-900">
+      <nav class="flex max-w-5xl m-auto p-1">
+        <div>
+          <a class="opacity-75 hover:opacity-100" href="/faq/">
+            <img class="p-1 h-14" src="/images/o!RL-logo.png" />
+          </a>
+        </div>
+        <div class="flex-auto self-center text-center">
+          <a class="p-1 opacity-75 hover:opacity-100" href="/lobbies/">
+            <span class="text-2xl">Lobbies</span>
           </a>
-          <a href="/leaderboard/osu/">
-            <img src="/images/leaderboard.png" />
-            <span>Leaderboard</span>
+          <i class="border opacity-75 ml-1 mr-2"> </i>
+          <a class="p-1 opacity-75 hover:opacity-100" href="/leaderboard/">
+            <span class="text-2xl">Leaderboard</span>
           </a>
         </div>
-        <div class="other-links">
-          <a href="https://kiwec.net/discord">
-            <img src="/images/discord.png" />
-            <span>Discord</span>
+        <div class="flex self-center">
+          <a class="p-2 opacity-75 hover:opacity-100" href="https://kiwec.net/discord">
+            <img class="h-8" src="/images/discord.png" />
           </a>
-          <a href="https://github.com/kiwec/osu-ranked-lobbies">
-            <img src="/images/github.png" />
-            <span>Github</span>
+          <div class="relative">
+            <button id="toggle-rulesets-dropdown-btn" class="p-2 opacity-75 hover:opacity-100">
+              <img class="h-8" src="/images/mode-osu.png" />
+            </button>
+            <div id="rulesets-dropdown" class="absolute hidden z-10 mt-4 -ml-12 bg-zinc-700 rounded-md">
+              <button class="choose-ruleset flex p-2 w-36 opacity-75 hover:opacity-100 hover:bg-zinc-900" data-ruleset="0">
+                <img class="h-8" src="/images/mode-osu.png" />
+                <span class="px-2 leading-7">osu!</span>
+              </button>
+              <button class="choose-ruleset flex p-2 w-36 opacity-75 hover:opacity-100 hover:bg-zinc-900" data-ruleset="1">
+                <img class="h-8" src="/images/mode-taiko.png" />
+                <span class="px-2 leading-7">osu!taiko</span>
+              </button>
+              <button class="choose-ruleset flex p-2 w-36 opacity-75 hover:opacity-100 hover:bg-zinc-900" data-ruleset="2">
+                <img class="h-8" src="/images/mode-catch.png" />
+                <span class="px-2 leading-7">osu!catch</span>
+              </button>
+              <button class="choose-ruleset flex p-2 w-36 opacity-75 hover:opacity-100 hover:bg-zinc-900" data-ruleset="3">
+                <img class="h-8" src="/images/mode-mania.png" />
+                <span class="px-2 leading-7">osu!mania</span>
+              </button>
+            </div>
+          </div>
+
+          <a class="login-btn p-2 opacity-75 hover:opacity-100" href="/osu_login">
+            <img class="h-8 rounded-full" src="/images/login.png" />
           </a>
         </div>
       </nav>
-    </div>
+    </header>
 
-    <main>
+    <main class="p-8">
       {{ error }}
       <noscript>Sorry, but to reduce server load, pages are rendered client-side, with JavaScript :(</noscript>
     </main>
 
     <template id="FAQ-template">
-      <h1>FAQ</h1>
-      <div class="Faq-info">
-      <h3>Commands for ranked lobbies</h3>
-      <ul>
-        <li>
-          !info or !discord - Display some information for new players.
-          </li>
-          <li>
-            !start -  Count down 30 seconds then start the map. Useful when some players are AFK or forget to ready up. Anybody can use this command.
-            </li>
-            <li>
-              !wait - Cancel !start. Use it when you're not done downloading.
-            </li>
-            <li>
-              !skip - Skip current map. You must have played 5 games in the lobby to unlock the command.
-            </li>
-            <li>
-              !abort - Vote to abort the match. At least 1/4 of the lobby must vote to abort for a match to get aborted.
-            </li>
-            <li>
-              !ban (player) - Vote to ban a player. You should probably use the in-game ignoring and reporting features instead.
-            </li>
-            <li>
-              !rank (player) - Display your rank or the rank of another player.
-            </li>
-      </ul>
-      <h3>Commands for unranked lobbies</h3>
-      <ul>
-        <li>
-          !collection (id) -  Switches to another collection. Only the lobby creator can use this command.
-        </li>
-        <li>
-          !start - Count down 30 seconds then start the map. Useful when some players are AFK or forget to ready up. Anybody can use this command.
-        </li>
-        <li>
-          !wait - Cancel !start. Use it when you're not done downloading.
-        </li>
-        <li>
-          !abort - Vote to abort the match. At least half the players in the lobby must vote to abort for a match to get aborted.
-        </li>
-        <li>
-          !skip - Vote to skip the current map. At least half the players in the lobby must vote to skip for a map to get skipped.
-        </li>
-      </ul>
-      <br>
-      <h2>Frequently Asked Questions</h2>
-      <br>
-      <h3>When do I get a rank?</h3>
+      <div class="max-w-5xl m-auto">
+        <h2 class="mb-2 underline">Frequently Asked Questions</h2>
+        <h3 class="mt-1">When do I get a rank?</h3>
         You get a rank after playing 5 games in a ranked lobby. To have it visible in this Discord server, you need to link your account in welcome.
-      <h3>How do the ranks work?</h3>
-        When you pass a map with 95% accuracy, you gain rank. When you don't, you lose rank. How much you gain or lose depends on your rank, as well as how hard the map is.
-      <h3>Will mods make me rank up faster?</h3>
+        <h3 class="mt-1">What are the ranks?</h3>
+        <table class="mt-1 mb-2">
+          <thead>
+            <tr><th class="border border-zinc-700 p-1.5">Division</th><th class="border border-zinc-700 p-1.5">Placement</th></tr>
+          </thead>
+          <tbody>
+            <tr><td class="border border-zinc-700 p-1.5 the-one">The One</td><td class="border border-zinc-700 p-1.5">#1</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 rhythm-incarnate pr-3">Rhythm Incarnate</td><td class="border border-zinc-700 p-1.5">Top 2.5%</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 diamond">Diamond</td><td class="border border-zinc-700 p-1.5">Top 10.1%</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 platinum">Platinum</td><td class="border border-zinc-700 p-1.5">Top 22.3%</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 gold">Gold</td><td class="border border-zinc-700 p-1.5">Top 38.3%</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 silver">Silver</td><td class="border border-zinc-700 p-1.5">Top 56.8%</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 bronze">Bronze</td><td class="border border-zinc-700 p-1.5">Top 75.5%</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 wood">Wood</td><td class="border border-zinc-700 p-1.5">Top 91.3%</td></tr>
+            <tr><td class="border border-zinc-700 p-1.5 cardboard">Cardboard</td><td class="border border-zinc-700 p-1.5">Bottom 8.6%</td></tr>
+          </tbody>
+        </table>
+        <h3 class="mt-1">How do the ranks work?</h3>
+        When you pass a map with 95% accuracy, you gain rank. When you don't, you lose rank. How much you gain or lose depends on your rank underline, as well as how hard the map is.
+        <h3 class="mt-1">Will mods make me rank up faster?</h3>
         No. Using difficulty-reducing mods will invalidate your score; only HD, HR, MR, SD/PF, FI/FL are allowed.
-      <h3>How are the maps chosen?</h3>
+        <h3 class="mt-1">How are the maps chosen?</h3>
         Maps are picked to best fit your current skill level.
-      <h3> What's the map pool?</h3>
+        <h3 class="mt-1"> What's the map pool?</h3>
         Initially, the map pool consists of collections from map-pool. Over time, it expands to cover the entirety of osu! maps with a leaderboard (about 130k maps).
-      <h3>Why is the star rating different from the title of the lobby?</h3>
+        <h3 class="mt-1">Why is the star rating different from the title of the lobby?</h3>
         Some maps are much easier/harder than their star rating represents. Also, the star rating shown by the client in multi lobbies is different from the current one (as shown on the osu! website).
-      <h3>What are the ranks?</h3>
-      <h4>Here is the rank distribution:</h4>
-      <ul class="Faq-Ranks">
-        <li> Cardboard: Bottom 8.6% </li>
-        <li> Wood: Top 91.3%</li>
-        <li> Bronze: Top 75.5%</li>
-        <li> Silver: Top 56.8%</li>
-        <li> Gold: Top 38.3%</li>
-        <li> Platinum: Top 22.3%</li>
-        <li> Diamond: Top 10.1%</li>
-        <li> Rhythm Incarnate: Top 2.5%</li>
-        <li> The One: #1</li>
-      </ul>
-      <h3>Why isn't the game starting when all players are ready?</h3>
+        <h3 class="mt-1">Why isn't the game starting when all players are ready?</h3>
         That happens the last person that wasn't ready leaves. Anyone can unready and re-ready to start the game immediately. (I can't fix this bug, it comes from BanchoBot itself.)'
-      <h3>Can I see the source code?</h3>
-        Yes: <a href="https://github.com/kiwec/osu-ranked-lobbies">https://github.com/kiwec/osu-ranked-lobbies</a>
+        <h3 class="mt-1">Can I see the source code?</h3>
+        Yes: <a href="https://github.com/kiwec/osu-ranked-lobbies" target="_blank">https://github.com/kiwec/osu-ranked-lobbies</a>
+
+        <h2 class="mb-2 mt-8 underline">Commands for ranked lobbies</h3>
+        <div class="ranked"></div>
+        <div>
+        <h2 class="mb-2 mt-8 underline">Commands for unranked lobbies</h3>
+        <div class="unranked"></div>
       </div>
     </template>
 
     <template id="lobbies-template">
-      <h1>Lobbies</h1>
-      <div class="lobby-creation-banner">
-        <span style="margin: auto 10px auto 20px">Not satisfied?</span><button class="go-to-create-lobby">Create your own!</button>
+      <div class="max-w-5xl m-auto">
+        <div class="lobby-creation-banner border-2 border-orange-600 rounded-lg p-3 text-center mx-2 mb-2">
+          <span class="pl-2 pr-4">Not satisfied?</span><button class="go-to-create-lobby default-btn">Create your own!</button>
+        </div>
+        <div class="lobby-list flex flex-wrap"></div>
       </div>
-      <div class="lobby-list"></div>
     </template>
 
     <template id="leaderboard-template">
-      <h1>Leaderboard</h1>
-      <span class="modes">
-        <a href="/leaderboard/osu/">
-          <img src="/images/mode-osu.png" ruleset="osu" >
-        </a>
-        <a href="/leaderboard/taiko/">
-         <img src="/images/mode-taiko.png" ruleset="taiko">
-        </a>
-          <a href="/leaderboard/catch/">
-        <img src="/images/mode-catch.png" ruleset="catch">
-        </a>
-        <a href="/leaderboard/mania/">
-          <img src="/images/mode-mania.png" ruleset="mania">
-        </a>
-      </span>
-      <div class="subheading">
-        <span class="nb-ranked"></span>
-      </div>
-      <div class="leaderboard-section">
-        <div class="leaderboard-focus">
-          <p class="ranking">The One</p>
+      <div class="max-w-5xl m-auto">
+        <div class="leaderboard-section">
+          <table class="leaderboard table-auto block">
+            <tbody class="flex flex-col">
+            </tbody>
+          </table>
+          <div class="pagination"></div>
         </div>
-        <table class="leaderboard">
-          <tbody>
-          </tbody>
-        </table>
-        <div class="pagination"></div>
       </div>
     </template>
 
     <template id="user-template">
-      <div class="heading">
-        <div class="heading-left"><img /></div>
-        <div class="heading-right">
-          <h1></h1>
-          <a class="subheading">
-            <span class="link_text">osu! profile</span>
-          </a>
+      <div class="flex max-w-5xl m-auto">
+        <div class="heading-left"><img class="h-24 rounded-full" /></div>
+        <div class="heading-right ml-8">
+          <h1 class="ml-2 mt-1" style="line-height:1.5em"></h1>
+          <a class="subheading ml-4" target="_blank">osu! profile</a>
         </div>
-        <span class="user-modes">
-          <a href="./osu/">
-            <img class="modes" src="/images/mode-osu.png" ruleset="osu" >
-          </a>
-          <a href="./taiko/">
-           <img class="modes" src="/images/mode-taiko.png" ruleset="taiko">
-          </a>
-            <a href="./catch/">
-          <img class="modes" src="/images/mode-catch.png" ruleset="catch">
-          </a>
-          <a href="./mania/">
-            <img class="modes" src="/images/mode-mania.png" ruleset="mania">
-          </a>
-        </span>
       </div>
-      <div class="user-section">
-        <div class="user-focus">
-          <div class="user-focus-block"></div>
-          <div class="user-focus-block"></div>
-          <div class="user-focus-block"></div>
+      <div class="user-section max-w-5xl m-auto">
+        <div class="user-focus flex flex-wrap my-4">
+          <div class="user-focus-block border-2 border-orange-600 rounded-lg inline-flex flex-col flex-grow p-4 m-4 text-center"></div>
+          <div class="user-focus-block border-2 border-orange-600 rounded-lg inline-flex flex-col flex-grow p-4 m-4 text-center"></div>
+          <div class="user-focus-block border-2 border-orange-600 rounded-lg inline-flex flex-col flex-grow p-4 m-4 text-center"></div>
         </div>
         <h2>Match History</h2>
-        <table class="match-history">
-          <thead>
-            <tr>
-              <td class="map">Map</td>
-              <td>Result</td>
-              <td>Time</td>
+        <table class="match-history block my-4">
+          <thead class="flex flex-col">
+            <tr class="inline-flex justify-between">
+              <td class="map grow w-40 p-1.5 text-center font-bold">Map</td>
+              <td class="w-40 p-1.5 text-center font-bold">Result</td>
+              <td class="w-40 p-1.5 text-center font-bold">Time</td>
             </tr>
           </thead>
-          <tbody></tbody>
+          <tbody class="flex flex-col"></tbody>
         </table>
         <div class="pagination"></div>
       </div>
     </template>
 
+    <template id="ranked-command-list-template">
+      <div class="command-list">
+        <ul>
+          <li>
+            !info or !discord - Display some information for new players.
+            </li>
+            <li>
+              !start -  Count down 30 seconds then start the map. Useful when some players are AFK or forget to ready up. Anybody can use this command.
+            </li>
+            <li>
+              !wait - Cancel !start. Use it when you're not done downloading.
+            </li>
+            <li>
+              !skip - Skip current map. You must have played 5 games in the lobby to unlock the command.
+            </li>
+            <li>
+              !abort - Vote to abort the match. At least 1/4 of the lobby must vote to abort for a match to get aborted.
+            </li>
+            <li>
+              !ban (player) - Vote to ban a player. You should probably use the in-game ignoring and reporting features instead.
+            </li>
+            <li>
+              !rank (player) - Display your rank or the rank of another player.
+            </li>
+        </ul>
+      </div>
+    </template>
+
+    <template id="unranked-command-list-template">
+      <div class="command-list">
+        <ul>
+          <li>
+            !collection (id) -  Switches to another collection. Only the lobby creator can use this command.
+          </li>
+          <li>
+            !start - Count down 30 seconds then start the map. Useful when some players are AFK or forget to ready up. Anybody can use this command.
+          </li>
+          <li>
+            !wait - Cancel !start. Use it when you're not done downloading.
+          </li>
+          <li>
+            !abort - Vote to abort the match. At least half the players in the lobby must vote to abort for a match to get aborted.
+          </li>
+          <li>
+            !skip - Vote to skip the current map. At least half the players in the lobby must vote to skip for a map to get skipped.
+          </li>
+        </ul>
+      </div>
+    </template>
+
     <template id="lobby-creation-template">
-      <h1 style="margin-bottom:30px">New lobby</h1>
+      <div class="max-w-5xl m-auto">
+        <h1 class="mb-6 font-bold">New lobby</h1>
 
-      <div class="lobby-settings">
-        <h3>Ruleset</h3>
-        <div class="radio-area">
-          <div class="radio"><input type="radio" name="ruleset" value="0" checked></div>
-          <div class="info">
-            <h2>osu!</h2>
-            <p>The mode where circles get clicked</p>
+        <div class="lobby-settings">
+          <h3 class="underline mb-4">Lobby type</h3>
+          <!-- TODO: on firefox, cursor isn't "pointer" when hovering .radio; even worse, clicking .radio doesn't change the lobby type! -->
+          <div class="radio-area flex border-2 border-orange-600 hover:bg-zinc-700 rounded-md m-1 max-w-xl cursor-pointer">
+            <div class="radio flex align-center justify-center p-1.5 bg-orange-600"><input type="radio" name="lobby-type" value="ranked" checked></div>
+            <div class="info py-2 px-3 leading-5">
+              <span class="font-bold">Ranked</span>
+              <p>Map is selected automatically. Scores set in this lobby will appear on the o!RL leaderboard.</p>
+            </div>
           </div>
-        </div>
-        <div class="radio-area">
-          <div class="radio"><input type="radio" name="ruleset" value="1"></div>
-          <div class="info">
-            <h2>osu!taiko</h2>
-            <p>Tap drums to the rythm</p>
+          <div class="radio-area flex border-2 border-orange-600 hover:bg-zinc-700 rounded-md m-1 max-w-xl cursor-pointer">
+            <div class="radio flex align-center justify-center p-1.5 bg-orange-600"><input type="radio" name="lobby-type" value="custom"></div>
+            <div class="info py-2 px-3 leading-5">
+              <span class="font-bold">Custom</span>
+              <p>Personalized map selection, but scores set in this lobby will not change your o!RL rank.</p>
+            </div>
           </div>
-        </div>
-        <div class="radio-area">
-          <div class="radio"><input type="radio" name="ruleset" value="2"></div>
-          <div class="info">
-            <h2>osu!catch</h2>
-            <p>Catch fruits to the beat</p>
-          </div>
-        </div>
-        <div class="radio-area">
-          <div class="radio"><input type="radio" name="ruleset" value="3"></div>
-          <div class="info">
-            <h2>osu!mania 4k</h2>
-            <p>Piano simulator, but with only 4 keys</p>
-          </div>
-        </div>
 
-        <h3>Lobby type</h3>
-        <div class="radio-area">
-          <div class="radio"><input type="radio" name="lobby-type" value="ranked" checked></div>
-          <div class="info">
-            <h2>Ranked</h2>
-            <p>Map is selected automatically. Scores set in this lobby will appear on the o!RL leaderboard.</p>
+          <h3 class="underline mt-4 mb-2">Star rating</h3>
+          <div>
+            <label>
+              <input type="checkbox" name="auto-star-rating" checked>
+              Automatically choose star rating based on lobby players
+            </label>
           </div>
-        </div>
-        <div class="radio-area">
-          <div class="radio"><input type="radio" name="lobby-type" value="custom"></div>
-          <div class="info">
-            <h2>Custom</h2>
-            <p>Personalized map selection, but scores set in this lobby will not change your o!RL rank.</p>
+          <div class="star-rating-range mb-2" hidden>
+            <label>
+              <input class="p-1 w-20 text-center text-black mt-2 mr-1" type="number" min="0" max="11" value="4.0" step="0.1" name="min-stars">
+              Minimum star level
+            </label>
+            <br>
+            <label>
+              <input class="p-1 w-20 text-center text-black mt-1 mr-1" type="number" min="0" max="11" value="5.0" step="0.1" name="max-stars">
+              Maximum star level
+            </label>
           </div>
-        </div>
 
-        <h3>Star rating</h3>
-        <div>
-          <label>
-            <input type="checkbox" name="auto-star-rating" checked>
-            Automatically choose star rating based on lobby players
-          </label>
-        </div>
-        <div class="star-rating-range" style="margin:10px" hidden>
-          <label>
-            <input type="number" min="0" max="11" value="4.0" step="0.1" name="min-stars">
-            Minimum star level
-          </label>
-          <br>
-          <label>
-            <input type="number" min="0" max="11" value="5.0" step="0.1" name="max-stars">
-            Maximum star level
-          </label>
-        </div>
+          <div class="custom-settings mt-4" hidden>
+            <h3 class="underline mb-1">Title</h3>
+            <p>This is what will be shown on the website's lobby list.</p>
+            <input class="mt-1 p-1 w-72 text-sm" type="text" name="title" placeholder="New o!RL lobby">
 
-        <div class="custom-settings" hidden>
-          <h3>Title</h3>
-          <p>This is what will be shown on the website's lobby list.</p>
-          <input type="text" name="title" placeholder="New o!RL lobby">
+            <h3 class="underline mt-4 mb-1">Collection</h3>
+            <p>You can find or create a collection on <a href="https://osucollector.com/" target="_blank">osu!collector</a>.</p>
+            <input class="mt-1 p-1 w-72 text-sm" type="text" name="collection-url" placeholder="https://osucollector.com/collections/44">
+            <!-- TODO: More custom settings. filters, tags, etc. -->
+          </div>
 
-          <h3>Collection</h3>
-          <p>You can find or create a collection on <a href="https://osucollector.com/">osu!collector</a>.</p>
-          <input type="text" name="collection-url" placeholder="https://osucollector.com/collections/44">
-          <!-- TODO: More custom settings. filters, tags, etc. -->
+          <button class="create-lobby-btn default-btn mt-4">Create lobby</button>
         </div>
 
-        <button class="create-lobby-btn">Create lobby</button>
-      </div>
-
-      <div class="lobby-creation-spinner" hidden>
-        <p>Creating lobby...</p>
-        <div class="spinner"></div>
-      </div>
+        <div class="lobby-creation-spinner text-center mt-24" hidden>
+          <p>Creating lobby...</p>
+          <svg class="animate-spin m-auto my-4 h-10 w-10 text-orange-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
+            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+          </svg>
+        </div>
 
-      <div class="lobby-creation-error" hidden>
-        An error occurred while creating the lobby:
-        <div class="error-msg" style="margin-top:10px"></div>
+        <div class="lobby-creation-error" hidden>
+          An error occurred while creating the lobby:
+          <div class="error-msg mt-4"></div>
 
-        <button class="go-back-btn">Back</button>
-      </div>
+          <button class="go-back-btn default-btn mt-4">Back</button>
+        </div>
 
-      <div class="lobby-creation-need-ref" hidden>
-        <p>Unfortunately, the bot couldn't create the lobby automatically. This happens when there are already 10 bot-created lobbies.</p>
-        <p>But don't worry, you can still create it yourself! Just follow these steps:</p>
+        <div class="lobby-creation-need-ref" hidden>
+          <p>Unfortunately, the bot couldn't create the lobby automatically. This happens when there are too many bot-created lobbies.</p>
+          <p>But don't worry, you can still create it yourself! Just follow these steps:</p>
 
-        <ul>
-          <li>Create a lobby in osu!</li>
-          <li>In the #multiplayer chat, send <strong>!mp addref botkiwec</strong></li>
-          <li>BanchoBot should have sent a match history link. Copy the URL and paste it here.</li>
-        </ul>
+          <ul class="mb-2">
+            <li>Create a lobby in osu!</li>
+            <li>In the #multiplayer chat, send <strong>!mp addref botkiwec</strong></li>
+            <li>BanchoBot should have sent a match history link. Copy the URL and paste it here.</li>
+          </ul>
 
-        <input type="text" name="tournament-url" placeholder="https://osu.ppy.sh/community/matches/123456789">
-        <button class="create-lobby-btn">Create lobby</button>
-      </div>
+          <input class="mt-1 p-1.5 w-96 text-sm" type="text" name="tournament-url" placeholder="https://osu.ppy.sh/community/matches/123456789">
+          <button class="create-lobby-btn default-btn m-1 py-1">Create lobby</button>
+        </div>
 
-      <div class="lobby-creation-success" hidden>
-        <h3>Lobby created successfully!</h3>
-        <div class="lobby-list">
-          <div class="lobby"></div>
+        <div class="lobby-creation-success" hidden>
+          <h3 class="mt-4">Lobby created successfully!</h3>
+          <div class="lobby-list flex flex-wrap mt-4">
+            <div class="lobby"></div>
+          </div>
+          <div class="info mt-4"></div>
         </div>
       </div>
     </template>
 
-    <script src="/scripts.js?v=2.4" type="module"></script>
+    <script src="/scripts.js?v=2.5" type="module"></script>
   </body>
 </html>

+ 0 - 48
public/reset.css

@@ -1,48 +0,0 @@
-/* http://meyerweb.com/eric/tools/css/reset/ 
-   v2.0 | 20110126
-   License: none (public domain)
-*/
-
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed, 
-figure, figcaption, footer, header, hgroup, 
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
-	margin: 0;
-	padding: 0;
-	border: 0;
-	font-size: 100%;
-	font: inherit;
-	vertical-align: baseline;
-}
-/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure, 
-footer, header, hgroup, menu, nav, section {
-	display: block;
-}
-body {
-	line-height: 1;
-}
-ol, ul {
-	list-style: none;
-}
-blockquote, q {
-	quotes: none;
-}
-blockquote:before, blockquote:after,
-q:before, q:after {
-	content: '';
-	content: none;
-}
-table {
-	border-collapse: collapse;
-	border-spacing: 0;
-}

+ 153 - 218
public/scripts.js

@@ -1,15 +1,31 @@
-let m;
-let user_id = null;
+const rulesets = ['osu', 'taiko', 'catch', 'mania'];
+let selected_ruleset = parseInt(localStorage.getItem('selected_ruleset') || '0', 10);
 
+function update_selected_ruleset(name) {
+  if (name == 'std') name = 'osu';
+  if (name == 'fruits') name = 'catch';
+  if (!rulesets.includes(name)) name = 'osu';
 
-function fancy_elo(elo) {
-  if (elo == '???') {
-    return '???';
-  } else {
-    return Math.round(elo);
+  localStorage.setItem('selected_ruleset', rulesets.indexOf(name));
+  selected_ruleset = rulesets.indexOf(name);
+
+  document.querySelector('#toggle-rulesets-dropdown-btn img').src = `/images/mode-${rulesets[selected_ruleset]}.png`;
+}
+
+function update_header_highlights() {
+  const header_links = document.querySelectorAll('header a');
+  for (const link of header_links) {
+    if (location.pathname.includes(link.pathname)) {
+      link.classList.add('opacity-100');
+    } else {
+      link.classList.remove('opacity-100');
+    }
   }
 }
 
+let m;
+let user_id = null;
+
 
 // Returns the color of a given star rating, matching osu!web's color scheme.
 function stars_to_color(sr) {
@@ -36,6 +52,31 @@ function stars_to_color(sr) {
 }
 
 
+document.addEventListener('click', (event) => {
+  const open_ruleset_dropdown_btn = document.querySelector('#toggle-rulesets-dropdown-btn');
+  const ruleset_dropdown = document.querySelector('#rulesets-dropdown');
+  if (open_ruleset_dropdown_btn.contains(event.target)) {
+    ruleset_dropdown.classList.toggle('hidden');
+  } else {
+    ruleset_dropdown.classList.add('hidden');
+  }
+});
+
+document.querySelectorAll('.choose-ruleset').forEach((btn) => {
+  btn.addEventListener('click', function(event) {
+    event.preventDefault();
+    update_selected_ruleset(rulesets[parseInt(this.dataset.ruleset, 10)]);
+
+    let url = location.pathname;
+    for (const ruleset of rulesets) {
+      url = url.replaceAll(/\/(osu|taiko|catch|mania)/g, '/' + rulesets[selected_ruleset]);
+    }
+    window.history.pushState({}, '', url);
+    route(url);
+  });
+});
+
+
 function click_listener(evt) {
   if (this.pathname == '/osu_login') {
     document.cookie = 'redirect=' + location.pathname.split('/')[1];
@@ -46,9 +87,7 @@ function click_listener(evt) {
   if (this.host == location.host && this.target != '_blank') {
     evt.preventDefault();
 
-    console.log('Loading ' + this.href);
-    window.history.pushState({}, 'osu! ranked lobbies', this.href);
-    document.querySelector('main').innerHTML = '';
+    window.history.pushState({}, '', this.href);
     route(this.href);
   }
 };
@@ -66,12 +105,9 @@ async function get(url) {
   if (!user_id && res.headers.has('X-Osu-ID')) {
     user_id = parseInt(res.headers.get('X-Osu-ID'), 10);
 
-    const a = document.querySelector('.login_link');
-    a.setAttribute('class', 'profile_link');
-    a.href = '/u/' + user_id + '/';
-    a.innerHTML = `
-      <img src="https://s.ppy.sh/a/${user_id}" />
-      <span>Profile</span>`;
+    const a = document.querySelector('.login-btn');
+    a.href = `/u/${user_id}/${rulesets[selected_ruleset]}/`;
+    a.querySelector('img').src = `https://s.ppy.sh/a/${user_id}`;
   }
 
   const json = await res.json();
@@ -111,13 +147,15 @@ function render_pagination(node, page_num, max_pages, url_formatter) {
   const previous = Math.max(page_num - 1, 1);
   const next = Math.min(page_num + 1, max_pages);
   node.innerHTML = `
-    <a href="${url_formatter(previous)}"><span class="left-arrow">‹</span>Previous</a>
-    <div class="number-nav"></div>
-    <a href="${url_formatter(next)}">Next<span class="right-arrow">›</span></a>`;
+  <div class="flex justify-between m-5">
+    <a class="text-xl text-zinc-400 hover:text-zinc-50" href="${url_formatter(previous)}"><span class="text-2xl text-orange-600 mr-2">‹</span>Previous</a>
+    <div class="number-nav leading-10"></div>
+    <a class="text-xl text-zinc-400 hover:text-zinc-50" href="${url_formatter(next)}">Next<span class="text-2xl text-orange-600 ml-2">›</span></a>
+  </div>`;
   const numbers_div = node.querySelector('.number-nav');
   for (const page of pages) {
     numbers_div.innerHTML += `
-      <a ${page.is_current ? 'class="current-page"' : ''}
+      <a class="text-xl px-3 py-2 border-transparent border-2 ${page.is_current ? 'text-zinc-50 border-b-orange-600' : 'text-zinc-400 hover:text-zinc-50 hover:border-b-orange-400'}"
       href="${url_formatter(page.number)}">${page.number}</a>`;
   }
 }
@@ -132,15 +170,17 @@ function render_lobby(lobby) {
   }
 
   const color = stars_to_color(lobby.map ? lobby.map.stars : 0);
+  lobby_div.className = 'flex flex-1 m-2 rounded-md';
   lobby_div.style = `border: solid ${color} 2px`;
   lobby_div.innerHTML += `
-    <div class="lobby-info">
-      <div class="lobby-title"></div>
+    <div class="lobby-info min-w-[25rem] flex-1 p-2">
+      <div class="lobby-title font-bold"></div>
       <div>${stars} · ${lobby.nb_players}/16 players</div>
-      <div class="lobby-creator">Created by <a href="/u/${lobby.creator_id}"><img src="https://s.ppy.sh/a/${lobby.creator_id}" alt="Lobby creator"> ${lobby.creator_name}</a></div>
+      <div class="lobby-creator">Created by <a href="/u/${lobby.creator_id}"><img class="h-5 text-bottom rounded-full inline" src="https://s.ppy.sh/a/${lobby.creator_id}" alt="Lobby creator"> ${lobby.creator_name}</a></div>
     </div>
-    <div class="lobby-links" style="background-color:${color}">
-      <div><a href="/get-invite/${lobby.bancho_id}" target="_blank"><i class="fa-solid fa-xs fa-envelope"></i></a><span>Get invite</span></div>
+    <div class="lobby-links flex flex-col justify-evenly" style="background-color:${color}">
+      <div class="group relative text-center"><a class="!text-white text-2xl p-1.5 pl-2" href="osu://mp/${lobby.bancho_id}"><i class="fa-solid fa-xs fa-arrow-up-right-from-square"></i></a><span class="tooltip top-[-1.3rem]">Join (cutting edge only)</span></div>
+      <div class="group relative text-center"><a class="!text-white text-2xl p-1.5 pl-2" href="/get-invite/${lobby.bancho_id}" target="_blank"><i class="fa-solid fa-xs fa-envelope"></i></a><span class="tooltip top-[-0.1rem]">Get invite</span></div>
     </div>`;
   lobby_div.querySelector('.lobby-title').innerText = lobby.name;
   return lobby_div;
@@ -149,6 +189,10 @@ function render_lobby(lobby) {
 async function render_faq() {
   document.title = 'FAQ - o!RL';
   const template = document.querySelector('#FAQ-template').content.cloneNode(true);
+  const ranked_template = document.querySelector('#ranked-command-list-template').content.cloneNode(true);
+  const unranked_template = document.querySelector('#unranked-command-list-template').content.cloneNode(true);
+  template.querySelector('.ranked').appendChild(ranked_template);
+  template.querySelector('.unranked').appendChild(unranked_template);
   document.querySelector('main').appendChild(template);
 }
 
@@ -173,42 +217,34 @@ async function render_lobbies() {
       document.cookie = 'redirect=create-lobby';
       document.location = '/osu_login';
     } else {
-      console.log('Loading ' + '/create-lobby/');
-      window.history.pushState({}, 'osu! ranked lobbies', '/create-lobby/');
-      document.querySelector('main').innerHTML = '';
+      window.history.pushState({}, '', '/create-lobby/');
       route('/create-lobby/');
     }
   });
 }
 
 
+function fancy_elo(elo) {
+  if (elo == '???') {
+    return '???';
+  } else {
+    return Math.round(elo);
+  }
+}
+
 async function render_leaderboard(ruleset, page_num) {
   document.title = 'Leaderboard - o!RL';
   const json = await get(`/api/leaderboard/${ruleset}/${page_num}`);
 
   const template = document.querySelector('#leaderboard-template').content.cloneNode(true);
-  template.querySelector('.nb-ranked').innerText = `${json.nb_ranked_players} ranked players`;
-  template.querySelectorAll(`[ruleset^='${ruleset}']`).forEach((div) => {
-    div.style.filter = 'opacity(0.5)';
-  });
-
-  if (json.the_one) {
-    template.querySelector('.leaderboard-focus').innerHTML += `
-      <p class="username"><a href="/u/${json.the_one.user_id}/">${json.the_one.username}</a></p>
-      <p class="elo-value">${fancy_elo(json.the_one.elo)}</p>
-      <p class="elo">ELO</p>`;
-  } else {
-    template.querySelector('.leaderboard-focus').remove();
-  }
-
   const lboard = template.querySelector('.leaderboard tbody');
   for (const player of json.players) {
     lboard.innerHTML += `
-      <tr>
-        <td>${player.ranking}</td>
-        <td><a href="/u/${player.user_id}/">${player.username}</a></td>
-        <td>${fancy_elo(player.elo)}</td>
-        <td>ELO</td>
+      <tr class="inline-flex justify-between">
+        <td class="border border-transparent border-r-zinc-700 p-1.5 pr-3 w-10 text-right">${player.ranking}</td>
+        <td class="pl-3 p-1.5 ${player.ranking == 1 ? 'the-one': ''}"><a href="/u/${player.user_id}/">${player.username}</a></td>
+        <td class="p-1.5 ml-auto">${fancy_elo(player.elo)}</td>
+        <td class="p-1.5 text-orange-600">ELO</td>
       </tr>`;
   }
 
@@ -219,75 +255,66 @@ async function render_leaderboard(ruleset, page_num) {
 }
 
 
-async function render_user(user_id, ruleset, page_num) {
+async function render_user(user_id, page_num) {
   const json = await get('/api/user/' + user_id);
+  const user_info = json.ranks[selected_ruleset];
   document.title = `${json.username} - o!RL`;
 
-  const rulesets = ['osu', 'taiko', 'catch', 'mania'];
-  const rulesets2 = ['osu', 'taiko', 'fruits', 'mania'];
-  let mode = rulesets.indexOf(ruleset);
-  if (mode == -1) {
-    let best_rank = {nb_scores: -1};
-    for (const rank of json.ranks) {
-      if (rank.nb_scores > best_rank.nb_scores) {
-        best_rank = rank;
-      }
-    }
-    mode = best_rank.mode;
-    ruleset = rulesets[mode];
-  }
-
+  const division_to_class = {
+    'Unranked': 'unranked',
+    'Cardboard': 'cardboard',
+    'Wood': 'wood',
+    'Wood+': 'wood',
+    'Bronze': 'bronze',
+    'Bronze+': 'bronze',
+    'Silver': 'silver',
+    'Silver+': 'silver',
+    'Gold': 'gold',
+    'Gold+': 'gold',
+    'Platinum': 'platinum',
+    'Platinum+': 'platinum',
+    'Diamond': 'diamond',
+    'Diamond+': 'diamond',
+    'Rhythm Incarnate': 'rhythm-incarnate',
+    'The One': 'the-one',
+  };
 
+  const ruleset = rulesets[selected_ruleset];
   const template = document.querySelector('#user-template').content.cloneNode(true);
   template.querySelector('.heading-left img').src = `https://s.ppy.sh/a/${json.user_id}`;
   template.querySelector('.heading-right h1').innerText = json.username;
+  template.querySelector('.heading-right h1').classList.add(division_to_class[user_info.text]);
   template.querySelector('.heading-right .subheading').href = `https://osu.ppy.sh/users/${json.user_id}`;
   template.querySelectorAll('.user-modes a').forEach((div) => {
     div.href = `/u/${json.user_id}/${div.querySelector('img').getAttribute('ruleset')}`;
   });
-  template.querySelectorAll(`[ruleset^='${ruleset}']`).forEach((div) => {
-    div.style.filter = 'opacity(0.5)';
-  });
 
   const blocks = template.querySelectorAll('.user-focus-block');
-  if (json.ranks[mode].rank_nb >= 5) {
-    if (json.ranks[mode].text.includes('+')) {
-      json.ranks[mode].text = json.ranks[mode].text.replaceAll('+', '<span style=\'color:white;font-size:1em\'>+</span>');
-      blocks[0].innerHTML = `<span style="color: ${json.ranks[mode].rank_cr}">${json.ranks[mode].text}</span><span>Rank #${json.ranks[mode].rank_nb}</span>`;
-      blocks[1].innerHTML = `<span>${json.ranks[mode].nb_scores}</span><span>Games Played</span>`;
-      blocks[2].innerHTML = `<span>${fancy_elo(json.ranks[mode].elo)}</span><span>Elo</span>`;
-    } else if (json.ranks[mode].text == 'Rhythm Incarnate') {
-      blocks[0].innerHTML = `<span style="background: -webkit-linear-gradient(#c14790, #8c92bd); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: normal;">${json.ranks[mode].text}</span><span>Rank #${json.ranks[mode].rank_nb}</span>`;
-      blocks[1].innerHTML = `<span>${json.ranks[mode].nb_scores}</span><span>Games Played</span>`;
-      blocks[2].innerHTML = `<span>${fancy_elo(json.ranks[mode].elo)}</span><span>Elo</span>`;
-    } else if (json.ranks[mode].text == 'The One') {
-      blocks[0].innerHTML = `<span style="background: -webkit-linear-gradient(#ff0070, #f98c8c); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: normal;">${json.ranks[mode].text}</span><span>Rank #${json.ranks[mode].rank_nb}</span>`;
-      blocks[1].innerHTML = `<span>${json.ranks[mode].nb_scores}</span><span>Games Played</span>`;
-      blocks[2].innerHTML = `<span>${fancy_elo(json.ranks[mode].elo)}</span><span>Elo</span>`;
-    } else {
-      blocks[0].innerHTML = `<span style="color: ${json.ranks[mode].rank_cr}">${json.ranks[mode].text}</span><span>Rank #${json.ranks[mode].rank_nb}</span>`;
-      blocks[1].innerHTML = `<span>${json.ranks[mode].nb_scores}</span><span>Games Played</span>`;
-      blocks[2].innerHTML = `<span>${fancy_elo(json.ranks[mode].elo)}</span><span>Elo</span>`;
-    }
+  if (user_info.nb_scores >= 5) {
+    blocks[0].innerHTML = `<span class="text-3xl ${division_to_class[user_info.text]}">${user_info.text}</span><span class="text-xl p-1">Rank #${user_info.rank_nb}</span>`;
+    blocks[1].innerHTML = `<span class="text-orange-600 text-3xl">${user_info.nb_scores}</span><span class="text-xl p-1">Games Played</span>`;
+    blocks[2].innerHTML = `<span class="text-orange-600 text-3xl">${fancy_elo(user_info.elo)}</span><span class="text-xl p-1">Elo</span>`;
   } else {
-    blocks[0].innerHTML = `<span>Unranked</span><span>Rank #???</span>`;
-    blocks[1].innerHTML = `<span>${json.ranks[mode].nb_scores}</span><span>Games Played</span>`;
+    blocks[0].innerHTML = `<span class="text-3xl">Unranked</span><span class="text-xl p-1">Rank #???</span>`;
+    blocks[1].innerHTML = `<span class="text-orange-600 text-3xl">${user_info.nb_scores}</span><span class="text-xl p-1">Games Played</span>`;
     blocks[2].remove();
   }
   document.querySelector('main').appendChild(template);
 
   const matches_json = await get(`/api/user/${user_id}/${ruleset}/matches/${page_num}`);
   const tbody = document.querySelector('.match-history tbody');
+  const osu_rulesets = ['osu', 'taiko', 'fruits', 'mania'];
   for (const match of matches_json.matches) {
     const row = document.createElement('tr');
+    row.classList.add('inline-flex', 'justify-between');
     row.innerHTML = `
-      <td class="map">
-        <a href="https://osu.ppy.sh/beatmapsets/${match.map.set_id}#${rulesets2[mode]}/${match.map.id}"></a>
+      <td class="map grow w-40 p-1.5 text-center border border-transparent border-t-zinc-700">
+        <a href="https://osu.ppy.sh/beatmapsets/${match.map.set_id}#${osu_rulesets[selected_ruleset]}/${match.map.id}"></a>
       </td>
-      <td ${match.won ? 'class="green"' : 'class="red"'}>
+      <td class="w-40 p-1.5 text-center border border-transparent border-t-zinc-700 ${match.won ? 'text-green-600' : 'text-red-600'}">
         ${match.won ? 'WON' : 'LOST'}
       </td>
-      <td data-tms="${match.tms}">${match.time}</td>`;
+      <td class="w-40 p-1.5 text-center border border-transparent border-t-zinc-700" data-tms="${match.tms}">${match.time}</td>`;
     row.querySelector('.map a').innerText = match.map.name;
     tbody.appendChild(row);
   }
@@ -298,9 +325,12 @@ async function render_user(user_id, ruleset, page_num) {
 
 
 async function route(new_url) {
+  console.info('Loading ' + new_url);
+  document.querySelector('main').innerHTML = '';
+  update_header_highlights();
+
   if (new_url == '/osu_login') {
     document.location = '/osu_login';
-    location.reload();
     return;
   }
 
@@ -308,6 +338,8 @@ async function route(new_url) {
     document.title = 'New lobby - o!RL';
     document.querySelector('main').innerHTML = '';
     const template = document.querySelector('#lobby-creation-template').content.cloneNode(true);
+    console.log(template);
+    template.querySelector('h1').innerText = `New ${rulesets[selected_ruleset]} lobby`;
     document.querySelector('main').appendChild(template);
 
     document.querySelector('.lobby-settings').addEventListener('change', (evt) => {
@@ -334,10 +366,11 @@ async function route(new_url) {
       document.querySelector('main .lobby-creation-need-ref').hidden = true;
       document.querySelector('main .lobby-creation-spinner').hidden = false;
 
+      const lobby_type = document.querySelector('input[name="lobby-type"]:checked').value;
       try {
         const lobby_settings = {
-          type: document.querySelector('input[name="lobby-type"]:checked').value,
-          ruleset: document.querySelector('input[name="ruleset"]:checked').value,
+          type: lobby_type,
+          ruleset: selected_ruleset,
           star_rating: document.querySelector('main input[name="auto-star-rating"]').checked ? 'auto' : 'fixed',
           min_stars: parseFloat(document.querySelector('main input[name="min-stars"]').value),
           max_stars: parseFloat(document.querySelector('main input[name="max-stars"]').value),
@@ -374,6 +407,9 @@ async function route(new_url) {
 
         document.querySelector('.lobby-creation-spinner').hidden = true;
         document.querySelector('.lobby-creation-success .lobby').outerHTML = render_lobby(json_res.lobby).outerHTML;
+
+        const info_template = document.querySelector(lobby_type == 'ranked' ? '#ranked-command-list-template' : '#unranked-command-list-template').content.cloneNode(true);
+        document.querySelector('.lobby-creation-success .info').appendChild(info_template);
         document.querySelector('.lobby-creation-success').hidden = false;
       } catch (err) {
         document.querySelector('.lobby-creation-error .error-msg').innerText = err.message;
@@ -381,36 +417,43 @@ async function route(new_url) {
         document.querySelector('.lobby-creation-error').hidden = false;
       }
     }));
+  } else if (m = new_url.match(/\/faq\//)) {
+    document.querySelector('main').innerHTML = '';
+    await render_faq();
   } else if (m = new_url.match(/\/lobbies\//)) {
     document.querySelector('main').innerHTML = '';
     await render_lobbies();
-  } else if (m = new_url.match(/\/leaderboard\/(.+)\/(page-(\d+)\/)?/)) {
+  } else if (m = new_url.match(/\/leaderboard\/(\w+)\/(page-(\d+)\/)?/)) {
     const ruleset = m[1];
+    update_selected_ruleset(ruleset);
+
     const page_num = m[3] || 1;
     document.querySelector('main').innerHTML = '';
     await render_leaderboard(ruleset, page_num);
-  } else if (m = new_url.match(/\/leaderboard\/(page-(\d+)\/)?/)) {
-    const page_num = m[2] || 1;
-    document.querySelector('main').innerHTML = '';
-    await render_leaderboard('osu', page_num);
-  } else if (m = new_url.match(/\/u\/(\d+)\/(.+)\/page-(\d+)\/?/)) {
-    const user_id = m[1];
+  } else if (m = new_url.match(/\/u\/(\d+)\/(\w+)\/page-(\d+)\/?/)) {
     const ruleset = m[2];
+    update_selected_ruleset(ruleset);
+
+    const user_id = m[1];
     const page_num = m[3] || 1;
     document.querySelector('main').innerHTML = '';
-    await render_user(user_id, ruleset, page_num);
-  } else if (m = new_url.match(/\/faq\//)) {
-    document.querySelector('main').innerHTML = '';
-    await render_faq();
-  } else if (m = new_url.match(/\/u\/(\d+)\/(.+)\/?/)) {
-    const user_id = m[1];
+    await render_user(user_id, page_num);
+  } else if (m = new_url.match(/\/u\/(\d+)\/(\w+)\/?/)) {
     const ruleset = m[2];
-    document.querySelector('main').innerHTML = '';
-    await render_user(user_id, ruleset, 1);
-  } else if (m = new_url.match(/\/u\/(\d+)\/?/)) {
+    update_selected_ruleset(ruleset);
+
     const user_id = m[1];
     document.querySelector('main').innerHTML = '';
-    await render_user(user_id, null, 1);
+    await render_user(user_id, 1);
+  } else if (m = new_url.match(/\/leaderboard\/(page-(\d+)\/)?/)) {
+    const page_num = m[2] || 1;
+    new_url = `/leaderboard/${rulesets[selected_ruleset]}/page-${page_num}/`;
+    window.history.replaceState({}, 'osu! ranked lobbies', new_url);
+    route(new_url);
+  } else if (m = new_url.match(/\/u\/(\d+)\/?/)) {
+    new_url = `/u/${m[1]}/${rulesets[selected_ruleset]}/`;
+    window.history.replaceState({}, 'osu! ranked lobbies', new_url);
+    route(new_url);
   } else {
     const main = document.querySelector('main');
     if (main.innerHTML.indexOf('{{ error }}') != -1) {
@@ -435,113 +478,5 @@ async function route(new_url) {
 }
 
 
-// User search
-const searchResults = document.querySelector('.search-results');
-const searchField = document.querySelector('.search-button');
-const searchFieldInput = document.querySelector('.search-button input');
-const searchFieldBackground = document.querySelector('.search-button + .search-background');
-if (searchField) {
-  searchField.addEventListener('click', () => searchFieldInput.focus());
-  searchFieldInput.addEventListener('focus', (ev) => {
-    let classes = searchField.getAttribute('class');
-    if (classes.indexOf('active') === -1) {
-      classes += ' active';
-      searchField.setAttribute('class', classes);
-    }
-  });
-
-  const searchTimeout = 400;
-  const lastSearchRequest = {
-    tms: null,
-    job: null,
-  };
-
-  searchField.addEventListener('input', (ev) => {
-    searchResults.innerHTML = '';
-    const searchQuery = ev.target.value;
-    clearTimeout(lastSearchRequest.job);
-    if (searchQuery === '') {
-      return;
-    };
-    lastSearchRequest.job = setTimeout(() => {
-      fetch(`/search?query=${searchQuery}`)
-          .then((res) => res.json())
-          .then((res) => {
-            res.forEach((player) => {
-              player.username = player.username.length > 20 ? (player.username.substr(0, 20)+'...') : player.username;
-              searchResults.innerHTML += `
-              <a href="/u/${player.user_id}" class="search-result-item">
-                <span>${player.username}</span>
-              </a>
-            `;
-            });
-            if (res.length === 0) {
-              searchResults.innerHTML = `
-              <div class="search-result-item not-found">
-                Nothing found!
-              </div>
-            `;
-            }
-          });
-    }, searchTimeout);
-    lastSearchRequest.tms = Date.now();
-  });
-
-  searchFieldBackground.addEventListener('click', () => {
-    searchField.setAttribute('class', searchField.getAttribute('class').replace('active', '').trim());
-  });
-}
-
-document.addEventListener('keydown', (event) => {
-  if (!document.querySelector('.search-button').classList.contains('active')) return;
-
-  if (event.key === 'Enter') {
-    const activeItem = document.querySelector('.search-result-item.active');
-    if (activeItem) {
-      document.location = activeItem.getAttribute('href');
-    }
-  }
-  const changeActiveItem = (isDown) => {
-    if (searchFieldInput === document.activeElement) {
-      searchFieldInput.blur();
-    }
-    const items = document.querySelectorAll('.search-result-item');
-    if (!document.querySelector('.search-result-item.active')) {
-      items[0].setAttribute('class', items[0].getAttribute('class')+' active');
-      return;
-    }
-    for (let i = 0; i < items.length; i++) {
-      if (items[i].getAttribute('class').indexOf('active') !== -1) {
-        items[i].setAttribute('class', items[i].getAttribute('class').replace('active', '').trim());
-        let nextIndex;
-        if (isDown) {
-          nextIndex = ((i + 1) > (items.length - 1)) ? 0 : (i + 1);
-        } else {
-          nextIndex = ((i - 1) < 0) ? (items.length - 1) : (i - 1);
-        }
-        items[nextIndex].setAttribute('class', items[nextIndex].getAttribute('class')+' active');
-        return;
-      }
-    }
-  };
-  if (event.keyCode === 40) {
-    if (document.querySelector('.search-button.active')) {
-      event.preventDefault();
-    }
-    changeActiveItem(true);
-  }
-  if (event.keyCode === 38) {
-    if (document.querySelector('.search-button.active')) {
-      event.preventDefault();
-    }
-    changeActiveItem(false);
-  }
-  if (event.key === 'Escape') {
-    searchField.setAttribute('class', searchField.getAttribute('class').replace('active', '').trim());
-    searchFieldInput.blur();
-  }
-});
-
-
 // Load pages and hijack browser browsing
 route(location.pathname);

+ 79 - 951
public/stylesheet.css

@@ -1,972 +1,100 @@
-[hidden] {
-    display: none !important;
-}
-html {
-  height: 100%;
-}
-body {
-	display: flex;
-	flex-direction: row;
-	width: 100%;
-    min-height: 100%;
-	font-family: "Segoe UI", sans-serif;
-	font-size: 25px;
-    color: rgb(211, 211, 211);
-}
-.sidebar {
-	position: fixed;
-	top: 0;
-	left: 0;
-    z-index: 2;
-	display: flex;
-	flex-direction: column;
-	width: 180px;
-	height: 100%;
-	background-color: #eeeeee;
-    background-color: #292727;
-}
-.sidebar .logo {
-	margin-top: 45px;
-	margin-left: auto;
-	margin-right: auto;
-}
-.sidebar .logo img {
-	width: 90px;
-}
-.sidebar nav {
-	display: flex;
-	flex-direction: column;
-	justify-content: space-between;
-	height: 100%;
-	margin: 60px;
-}
-.sidebar nav .other-links,
-.sidebar nav .main-links {
-    display: flex;
-    flex-direction: column;
-    width: 100%;
-}
-.sidebar nav a {
-	width: 60px;
-	height: 60px;
-	margin-bottom: 30px;
-	border: solid transparent 0;
-	border-radius: 100%;
-}
-.sidebar nav a:hover {
-	background-color: white;
-}
-.sidebar nav a:last-of-type {
-	margin-bottom: 0;
-}
-.sidebar nav a img {
-	width: 30px;
-	margin: 15px;
-	height: auto;
-	filter: invert(82%) sepia(0%) saturate(1%) hue-rotate(334deg) brightness(92%) contrast(82%);
-}
-.sidebar nav a:hover img {
-	filter: invert(48%) sepia(49%) saturate(712%) hue-rotate(282deg) brightness(96%) contrast(84%);
-}
-.sidebar nav a.profile_link {
-    margin-bottom: 40px;
-}
-.sidebar nav a.profile_link:hover {
-	background-color: unset;
-}
-.sidebar nav a.profile_link img {
-    filter: none;
-    height: auto;
-    width: 40px;
-    margin-left: 10px;
-    margin-right: 10px;
-    border-radius: 100%;
-}
-.red {
-    color: red;
-}
-.green {
-    color: green;
-}
-button {
-    margin: 20px;
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
 
-    border: none;
-    border-radius: 5px;
-    background-color: #e3609a;
-    color: #fff;
-    padding: 10px 15px;
-    cursor: pointer;
-}
-button:hover {
-    background-color: #e176a5;
-}
-input[type="checkbox"] {
-    width: 15px;
-    height: 15px;
-    cursor: pointer;
-}
-input[type="number"] {
-    margin: 5px 0 0 0;
-    padding: 5px;
-    width: 40px;
-}
-.lobby-settings input[type="text"], .lobby-creation-need-ref input[type="text"] {
-    padding: 5px;
-    margin: 10px 0;
-    width: 300px;
-}
-label {
-    font-size: 0.8em;
-    user-select: none;
-    cursor: pointer;
-}
-main {
-	position: relative;
-	left: 180px;
-	padding: 60px 120px;
-}
-h1 {
-	font-size: 2em;
-	font-weight: 700;
-    margin-bottom: 30px;
-}
-h3 {
-    margin: 20px 0;
-    font-size: 1.2em;
-    text-decoration: underline;
-}
-.subheading {
-	margin-top: 30px;
-	margin-bottom: 40px;
-	font-size: 1em;
-	font-weight: 700;
-}
-nav a span {
-    position: relative;
-    top: calc(-97.5px/2);
-    left: 105px;
-    z-index: 999;
-    display: none;
-    padding: 10px 15px;
-    margin-left: -10px;
-    border: solid 0 transparent;
-    border-radius: 10px;
-    background-color: #e3609a;
-    font-size: .5em;
-    font-weight: 700;
-    color: white;
-    pointer-events: none;
-    transition: margin .3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 150ms;
-    background-color: #c94580;
-}
-nav a span::before {
-    content: "";
-    position: absolute;
-    width: 0;
-    height: 0;
-    bottom: calc(50% - 7.5px);
-    left: -7px;
-    border: 7px solid transparent;
-    border-left: none;
-    border-right-color: #e3609a;
-}
-nav a:hover span {
-    margin-left: 0;
-    display: inline-block;
-}
-main {
-    width: calc(100% - 420px);
-    background-color: #383636;
-}
-.leaderboard-section .leaderboard-focus {
-    display: flex;
-    justify-content: flex-start;
-    align-items: center;
-    padding: 45px 60px;
-    border: solid #e3609a 3px;
-    border-radius: 10px;
-    border-color: #c94580;
-}
-.leaderboard-section .leaderboard-focus p {
-    font-size: 1.5em;
-    font-weight: 700;
-}
-.leaderboard-section .leaderboard-focus p a {
-    color: black;
-    text-decoration: none;
-    color: rgb(211, 211, 211);
-}
-.leaderboard-section .leaderboard-focus p.ranking {
-    color: #e3609a;
-    color: #c94580;
-}
-.leaderboard-section .leaderboard-focus p.username {
-    margin-left: 30px;
-}
-.leaderboard-section .leaderboard-focus p.elo-value {
-    margin-left: auto;
-    font-size: 1em;
-}
-.leaderboard-section .leaderboard-focus p.elo {
-    margin-left: 30px;  
-    font-size: 1em;
-    font-weight: 600;
-    font-variant: small-caps;
-    text-transform: lowercase;
-    color: #c94580;
-}
-.leaderboard-section .leaderboard {
-    display: block;
-    margin-top: 60px;
-}
-tbody {
-    display: flex;
-    flex-direction: column;
-}
-tbody tr {
-    display: inline-flex;
-    justify-content: space-between;
-    padding: 15px 30px;
-    border: solid #eee 3px;
-    border-radius: 10px;
-}
-tbody tr:nth-of-type(odd) {
-    border-color: transparent;
-}
-tbody tr td {
-    font-size: 1em;
-    font-weight: 700;
-}
-tbody tr td a {
-    color: black;
-    text-decoration: none;
-    color: rgb(211, 211, 211);
-}
-
-.lobby-list {
-    width: 100%;
-    display: flex;
-    flex-wrap: wrap;
-}
-.lobby-list .lobby {
-    display: flex;
-    flex: 1;
-    border-radius: 10px;
-    margin: 10px;
-    min-width: 500px;
-}
-.lobby-info {
-    flex: 1;
-    padding: 10px;
-}
-.lobby-info div {
-    font-size: 0.8em;
-    margin-top: 3px;
-}
-.lobby-title {
-    font-weight: bold;
-    margin-bottom: 5px;
-}
-.lobby-creator a {
-    color: #c94580;
-    text-decoration: none;
-}
-.lobby-creator a img {
-    height: 20px;
-    width: 20px;
-    vertical-align: text-bottom;
-}
-.lobby-links {
-    display: flex;
-    flex-direction: column;
-    justify-content: space-evenly;
-    flex: 0 0 30px;
-    border-radius: 0 5px 5px 0;
-}
-.lobby-links div {
-    position: relative;
-    text-align: center;
-}
-.lobby-links a {
-    color: white;
-}
-.lobby-links div span {
-    position: absolute;
-    top: -4px;
-    left: 40px;
-    z-index: 999;
-    display: none;
-    padding: 10px 15px;
-    margin-left: -10px;
-    border: solid 0 transparent;
-    border-radius: 10px;
-    background-color: #e3609a;
-    font-size: .5em;
-    font-weight: 700;
-    color: white;
-    pointer-events: none;
-}
-.lobby-links div:last-child span {
-    top: -10px;
-}
-.lobby-links div span::before {
-    content: "";
-    position: absolute;
-    width: 0;
-    height: 0;
-    bottom: calc(50% - 7.5px);
-    left: -7px;
-    border: 7px solid transparent;
-    border-left: none;
-    border-right-color: #e3609a;
-}
-.lobby-links div:hover span {
-    margin-left: 0;
-    display: inline-block;
-}
-
-.radio-area {
-    display: flex;
-    border: solid #e3609a 2px;
-    border-radius: 10px;
-    margin: 10px;
-    max-width: 600px;
-
-    cursor: pointer;
-    user-select: none;
-}
-.radio-area:hover {
-    background-color: rgb(70, 70, 70);
-}
-.radio-area .radio {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    padding: 5px;
-
-    background-color: #e3609a;
-    border-radius: 5px 0 0 5px;
-}
-.radio-area .info {
-    padding: 10px;
-    font-size: 0.8em;
-}
-.radio-area .info h2 {
-    font-size: 1em;
-    font-weight: bold;
-}
-
-.leaderboard tbody tr td {
-    margin-left: 30px;
-}
-.leaderboard tbody tr td:first-of-type {
-    margin-left: 0;
-}
-.leaderboard tbody tr td:nth-of-type(3) {
-    margin-left: auto;
-}
-.leaderboard tbody tr td:last-of-type {
-    font-size: 1em;
-    font-weight: 600;
-    font-variant: small-caps;
-    text-transform: lowercase;
-    color: #c94580;
-}
-.pagination {
-    display: flex;
-    justify-content: space-between;
-    margin-top: 60px;
-}
-.pagination > a {
-    font-size: 1em;
-    font-weight: 700;
-    text-decoration: none;
-    color: rgb(211, 211, 211);
-}
-.pagination .left-arrow {
-    margin-right: 15px;
-    color: #c94580;
-}
-.pagination .right-arrow {
-    margin-left: 15px;
-    color: #c94580;
-}
-.pagination .number-nav a {
-    display:inline-block;
-    padding-bottom: 6px;
-    border-bottom: solid transparent 3px;
-    margin: 0 15px;
-    font-size: 1em;
-    font-weight: 700;
-    text-decoration: none;
-    color: #b5b5b5;
-}
-.pagination .number-nav a.current-page {
-    color: rgb(211, 211, 211);
-    border-bottom: solid #e3609a 3px;
-    transition: border .3s ease-in-out;
-}
-.heading {
-    display: flex;
-    justify-content: flex-start;
-    margin-bottom: 60px;
-}
-.heading .heading-left {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    width: 105px;
-    height: 105px;
-    padding: 7.5px;
-    border: solid 3px #eeeeee;
-    border-radius: 100%;
-}
-.heading .heading-left img {
-    width: 90px;
-    height: 90px;
-    border: solid 0 transparent;
-    border-radius: 100%;
-}
-.heading .heading-right {
-    margin-left: 60px;
-}
-.heading .heading-right .subheading {
-    margin-bottom: 0;
-    display: block;
-    color: unset;
-}
-.user-section .user-focus {
-    display: flex;
-}
-.user-section .user-focus .user-focus-block {
-    display: inline-flex;
-    flex-direction: column;
-    align-items: center;
-    flex-grow: 1;
-    padding: 45px 60px;
-    border: solid #c94580 3px;
-    border-radius: 10px;
-    font-size: 1em;
-    font-weight: 700;
-}
-.user-section .user-focus .user-focus-block:nth-of-type(2) {
-    margin: 0 60px;
-}
-.user-section .user-focus .user-focus-block span {
-    text-align: center;
-}
-.user-section .user-focus .user-focus-block span:first-of-type {
-    margin-bottom: 30px;
-    font-size: 2em;
-    color: #c94580;
-}
-h2 {
-    font-size: 1.5em;
-    font-weight: 700
-}
-.user-section h2 {
-    margin: 60px 0;
-}
-.user-section .match-history {
-    display: block;
-}
-thead {
-    display: flex;
-    flex-direction: column;
-}
-thead tr {
-    display: inline-flex;
-    justify-content: space-between;
-    padding: 15px 30px;
-    border: solid #c94580 3px;
-    border-radius: 10px;
-}
-thead tr td {
-    font-size: 1em;
-    font-weight: 700;
-}
-.match-history td {
-    display: inline-block;
-    width: 195px;
-    text-align: center;
-}
-.match-history .map {
-    flex-grow: 2;
-}
-
-.search-button {
-  cursor: pointer;
-  position: absolute;
-  z-index: 51;
-  right: 200px;
-  top: 63px;
-  height: 30px;
-  align-items: center;
-  border: 2px solid black;
-  background-color: #383636;
-  border-radius: 30px;
-  border-color: rgb(211, 211, 211);
-  padding: 5px;
-}
-.search-button .search-icon {
-  position: relative;
-  top:  4px;
-  left: 5px;
-  fill: rgb(211, 211, 211);
-}
-.search-button.active {
-  transition: background .3s ease-in-out;
-  border-color: #c94580 !important;
-}
-.search-button input {
-  position: relative;
-  vertical-align: top;
-  width: 0;
-  background-color: unset;
-  border: none;
-  outline: none;
-  color: rgb(211, 211, 211);
-  font-size: 20px;
-  transition: width .3s ease-in-out;
-}
-.search-button input:focus,
-.search-button.active input {
-  width: 350px;
-  margin: 3px 13px 0 8px;
-  cursor: text;
-}
-.search-results {
-  position: absolute;
-  box-sizing: border-box;
-  top: 115%;
-  left: 0;
-  background-color: #3f3e3e;
-  border-bottom-left-radius: 6px;
-  border-bottom-right-radius: 6px;
-  width: 100%;
-  display: none;
-}
-.search-button.active .search-results {
-  display: block;
-}
-.search-results .search-result-item {
-  display: flex;
-  border: 2px solid rgb(211, 211, 211);
-  color: rgb(211, 211, 211);
-  border-bottom: none;
-  justify-content: space-between;
-  text-decoration: none;
-  padding: 10px;
-  font-size: 20px;
-}
-.search-results .search-result-item:last-child {
-  border-bottom: 2px solid rgb(211, 211, 211);
-}
-.search-results .search-result-item:focus:not(.not-found),
-.search-results .search-result-item:hover:not(.not-found),
-.search-results .search-result-item.active:not(.not-found) {
-  color: #c94580 !important;
-  border-color: #c94580 !important;
-  border-width: 4px;
-}
-.search-results .search-result-item:focus:not(.not-found) + .search-result-item,
-.search-results .search-result-item:hover:not(.not-found) + .search-result-item,
-.search-results .search-result-item.active:not(.not-found) + .search-result-item {
-  border-top-color: #c94580;
-  border-top-width: 4px;
-}
-.search-button + .search-background {
-  display: none;
-  position: fixed;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  background-color: rgba(255, 255, 255, 0.2);
-  z-index: 50;
-}
-.search-button.active + .search-background {
-  display: block;
-}
-
-@media screen and (max-width:1400px) {
-    body {
-        font-size: 21px;
-    }
-    .sidebar {
-        width: 150px;
-    }
-    .sidebar nav {
-        margin: 45px;
-    }
-    nav a span {
-        left: 90px;
-    }
-    .sidebar .logo img {
-        width: 75px;
-    }
-    main {
-        left: 150px;
-        width: calc(100% - 330px);
-        padding: 45px 90px;
-    }
-    .subheading {
-        margin-top: 30px;
-        margin-bottom: 60px;
-    }
-    .leaderboard-section .leaderboard {
-        margin-top: 30px;
-    }
-    .pagination {
-        margin-top: 30px;
-    }
-    .heading {
-        margin-bottom: 45px;
-    }
-    .heading .heading-left {
-        width: 82.5px;
-        height: 82.5px;
-    }
-    .heading .heading-left img {
-        width: 75px;
-        height: 75px;
-    }
-    .heading .heading-right {
-        margin-left: 45px;
-    }
-    .user-section h2 {
-        margin: 45px 0;
-    }
-    .user-section .user-focus {
-        flex-wrap: wrap;
-    }
-    .user-section .user-focus .user-focus-block {
-        padding: 30px 45px;
-    }
-    .user-section .user-focus .user-focus-block:first-of-type {
-        width: 100%;
-        margin-bottom: 45px;
-    }
-    .user-section .user-focus .user-focus-block:nth-of-type(2) {
-        margin-left: 0;
-        margin-right: 45px;
-    }
-    .user-section .user-focus .user-focus-block span:first-of-type {
-        margin-bottom: 22.5px;
-    }
-    .switcher-container {
-      right: 70px;
-      top: 25px;
-    }
-    .search-button {
-      right: 160px;
-      top: 43px;
-    }
-}
-@media screen and (max-width:1200px) {
-    .leaderboard-section .leaderboard-focus {
-        flex-wrap: wrap;
-        padding: 30px 45px;
-    }
-    .leaderboard-section .leaderboard-focus p.ranking {
-        width: 50%;
-    }
-    .leaderboard-section .leaderboard-focus p.username {
-        width: 100%;
-        margin-top: 15px;
-        margin-left: 0;
-    }
-    .leaderboard-section .leaderboard-focus p.elo-value {
-        margin-top: 20px;
-        margin-left: 0;
-    }
-    .leaderboard-section .leaderboard-focus p.elo {
-        margin-top: 20px;
-        margin-left: 7.5px;
-    }
-    .match-history td {
-        font-size: .75em;
-    }
-}
-@media screen and (max-width:991px) {
-    body {
-        font-size: 19px;
-    }
-    .heading .heading-left {
-        width: 67.5px;
-        height: 67.5px;
-    }
-    .heading .heading-left img {
-        width: 62.5px;
-        height: 62.5px;
-    }
-    .heading .heading-right {
-        margin-left: 30px;
-    }
-    .user-section .user-focus .user-focus-block:first-of-type {
-        margin-bottom: 30px;
-    }
-    .user-section .user-focus .user-focus-block:nth-of-type(2) {
-        margin-right: 30px;
-    }
-    .user-section h2 {
-        margin: 30px 0;
-    }
-}
-@media screen and (max-width:900px) {
-    .sidebar .logo {
-        margin-top: 30px;
-    }
-    .sidebar nav {
-        align-items: center;
-        margin: 30px;
-    }
-    .sidebar nav a {
-        display: flex;
-        flex-direction: column;
-        width: 100%;
-        text-decoration: none;
-    }
-    .sidebar nav a:hover {
-        background-color: transparent;
-    }
-    .sidebar nav a img {
-        margin-left: auto;
-        margin-right: auto;
-    }
-    .sidebar nav a:last-of-type {
-        margin-bottom: 30px;
-    }
-    .sidebar nav a.profile_link img {
-      margin-left: auto;
-      margin-right: auto;
-    }
-    nav a span {
-        position: static;
-        display: inline-block;
-        opacity: 1;
-        pointer-events: auto;
-        margin-left: 0;
-        font-size: .5em;
-        text-align: center;
-    }
-    
-    body {
-        font-size: 18px;
-    }
-    main {
-        width: calc(100% - 240px);
-        padding: 30px 45px;
-    }
-    .subheading {
-        margin-top: 15px;
-        margin-bottom: 30px;
-    }
-    .leaderboard-section .leaderboard-focus {
-        padding: 30px;
-    }
-    .leaderboard-section .leaderboard {
-        margin-top: 15px;
-    }
-    .heading {
-        margin-bottom: 30px;
-    }
-    .heading .heading-left {
-        width: 50px;
-        height: 50px;
+@layer base {
+    html {
+        color: rgb(211, 211, 211);
+        font-family: "Segoe UI", sans-serif;
     }
-    .heading .heading-left img {
-        width: 50px;
-        height: 50px;
-    }
-    .switcher-container {
-      right: 30px;
-      top: 10px;
-    }
-    .search-button {
-      right: 110px;
-      top: 28px;
-    }
-}
-@media screen and (max-width:820px) {
-    .leaderboard tbody tr td:last-of-type {
-        display: none;
-    }
-}
-@media screen and (max-width:780px) {
-    .leaderboard tbody tr td:nth-of-type(3) {
-        display: none;
-    }
-    .leaderboard tbody tr {
-        justify-content: flex-start;
-    }
-    .pagination .number-nav {
-        display: none;
-    }
-    .user-section .user-focus .user-focus-block {
-        width: 100%;
-    }
-    .user-section .user-focus .user-focus-block:nth-of-type(2) {
-        margin-right: 0;
-        margin-bottom: 30px;
-    }
-    .match-history td:first-of-type {
-        display: none;
-    }
-    .match-history td:last-of-type {
-        display: none;
-    }
-    .switcher-container {
-        display: none;
-    }
-    .search-button {
-      display: none;
-    }
-}
-@media screen and (max-width:680px) {
-    .search-button {
-        box-sizing: border-box;
-        width: 100%;
-    }
-    .search-button input {
-        box-sizing: border-box;
-        width: 100%;
-    }
-    .search-button input:focus,
-    .search-button input:not(:placeholder-shown) {
-        width: 100%;
-    }
-}
-@media screen and (max-width:600px) {
-    .sidebar {
-        z-index: 49;
-        flex-direction: row;
-        justify-content: space-between;
-        width: calc(100% - 60px);
-        height: 50px;
-        padding: 15px 30px;
+
+    h1 {
+        @apply text-4xl;
     }
-    .sidebar .logo {
-        margin: 0;
-        margin-right: 20px;
+
+    h2 {
+        @apply text-3xl;
     }
-    .sidebar .logo img {
-        width: 50px;
+
+    h3 {
+        @apply text-2xl;
     }
-    .sidebar nav {
-        flex-direction: row;
-        margin: 0;
+
+    ul {
+        list-style: square;
+        @apply ml-4;
     }
-    .sidebar nav .other-links {
-        flex-direction: row;
+
+    table {
+        @apply border-collapse;
     }
-    .sidebar nav .other-links:last-child {
-        margin-left: 20px;
+
+    a[target="_blank"] {
+        @apply text-orange-500 hover:text-orange-400 hover:underline;
     }
-    .sidebar nav a img {
-        width: 25px;
-        margin-top: 7.5px;
-        margin-bottom: 7.5px;
+
+    .cardboard {
+        color: #ad8762;
     }
-    .sidebar nav a.profile_link {
-        margin-bottom: inherit;
+
+    .wood {
+        color: #966F33;
     }
-    .sidebar nav a.profile_link img {
-        width: 25px;
+
+    .bronze {
+        color: #CD7F32;
     }
-    nav a {
-        margin-bottom: 0 !important;
-        margin-left: 10px;
+
+    .silver {
+        color: #C0C0C0;
     }
-    nav a:first-child {
-        margin-left: 0;
+
+    .gold {
+        color: #FFD700;
     }
-    nav a span {
-        padding: 5px 7px;
+
+    .platinum {
+        color: #2ecc71;
     }
-    main {
-        position: static;
-        width: 100%;
-        margin-top: 80px;
-        padding: 30px;
+
+    .diamond {
+        color: #b9f2ff;
     }
-    .match-history td {
-        width: 120px;
+
+    .rhythm-incarnate {
+        color: transparent;
+        background-image: linear-gradient(#c14790, #8c92bd);
+        background-clip: text;
     }
-}
 
-.lobby-creation-spinner {
-    text-align: center;
-}
-.lobby-creation-spinner p {
-    margin: 50px 0 30px 0;
-}
-.spinner {
-  display: inline-block;
-  width: 50px;
-  height: 50px;
-  border: 10px solid #e176a5;
-  border-radius: 50%;
-  border-top-color: #e58eb3;
-  animation: spin 1s ease-in-out infinite;
-}
-@keyframes spin {
-  to {
-    transform: rotate(360deg);
-  }
-}
+    .the-one {
+        color: transparent;
+        background-image: linear-gradient(#ff0070, #f98c8c);
+        background-clip: text;
+    }
 
-/* Got lazy */
-.lobby-creation-need-ref p {
-    margin: 5px 0;
-}
-ul {
-    list-style: square;
-    margin: 10px 0;
-}
-li {
-    margin: 5px 0;
-}
-.lobby-creation-banner {
-    display: flex;
-    margin: 10px;
-    border: solid #e3609a 2px;
-    border-radius: 10px;
-}
-.go-to-create-lobby {
-    margin: 15px 5px;
-}
-.user-modes {
-    margin-left: 20px;
-}
-.user-modes a {
-    text-decoration: none;
-    text-decoration-color: transparent;
-}
-.modes a {
-    text-decoration: none;
-    text-decoration-color: transparent;
-}
-.modes img , .user-modes a img  {
-    width: 45px;
-}
+    .tooltip {
+        @apply absolute hidden group-hover:inline-block p-2 z-50 bg-orange-600 text-center text-white text-sm font-bold;
+        left: 2.5rem;
+        border: solid 0 transparent;
+        border-radius: 10px;
+        pointer-events: none;
+        width: 6rem;
+    }
 
-/* FAQ */
+    .tooltip::before {
+        content: '';
+        position: absolute;
+        width: 0;
+        height: 0;
+        bottom: calc(50% - 7.5px);
+        left: -7px;
+        border: 7px solid transparent;
+        border-left: none;
+        border-right-color: #ea580c;
+    }
 
-.Faq-info > a {
-    color: #c94580;
-}
-.Faq-info > ul > li {
-    line-height: 1.3;
-}
-.Faq-Ranks {
-    list-style-type: none;
-    color:#999999;
-    margin-left: 2%;
-  }
-.Faq-Ranks li:before {
-    content: '\2014';
-    position: absolute;
-    margin-left: -30px;
+    .default-btn {
+        @apply text-white bg-orange-600 hover:bg-orange-500 rounded-lg py-1.5 px-3;
+    }
 }

+ 8 - 0
tailwind.config.cjs

@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: ["public/*.{html,js}"],
+  theme: {
+    extend: {},
+  },
+  plugins: [],
+}

+ 0 - 13
website_api.js

@@ -80,7 +80,6 @@ async function get_leaderboard_page(ruleset, page_num) {
 
   const data = {
     nb_ranked_players: total_players.nb,
-    the_one: false,
     players: [],
     page: page_num,
     max_pages: nb_pages,
@@ -88,18 +87,6 @@ async function get_leaderboard_page(ruleset, page_num) {
 
   // Players
   let ranking = offset + 1;
-  if (ranking == 1) {
-    data.the_one = {
-      user_id: res[0].user_id,
-      username: res[0].username,
-      ranking: ranking,
-      elo: Math.round(res[0].elo),
-    };
-
-    res.shift();
-    ranking++;
-  }
-
   for (const user of res) {
     data.players.push({
       user_id: user.user_id,

+ 400 - 4
yarn.lock

@@ -107,6 +107,27 @@
   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
   integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
 
+"@nodelib/[email protected]":
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.5"
+    run-parallel "^1.1.9"
+
+"@nodelib/[email protected]", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.5"
+    fastq "^1.6.0"
+
 "@sapphire/async-queue@^1.1.4":
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.4.tgz#ae431310917a8880961cebe8e59df6ffa40f2957"
@@ -222,7 +243,21 @@ acorn-jsx@^5.3.1:
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
-acorn@^7.4.0:
+acorn-node@^1.8.2:
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
+  integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
+  dependencies:
+    acorn "^7.0.0"
+    acorn-walk "^7.0.0"
+    xtend "^4.0.2"
+
+acorn-walk@^7.0.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+  integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
+
+acorn@^7.0.0, acorn@^7.4.0:
   version "7.4.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@@ -288,6 +323,14 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
 aproba@^1.0.3:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@@ -301,6 +344,11 @@ are-we-there-yet@~1.1.2:
     delegates "^1.0.0"
     readable-stream "^2.0.6"
 
+arg@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
+  integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
 argparse@^1.0.7:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -348,6 +396,11 @@ better-sqlite3@^7.5.0:
     bindings "^1.5.0"
     prebuild-install "^7.0.0"
 
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
 bindings@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@@ -388,6 +441,13 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
+braces@^3.0.2, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
 buffer@^5.5.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@@ -406,6 +466,11 @@ callsites@^3.0.0, callsites@^3.1.0:
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
+camelcase-css@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+  integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
 cargo-cp-artifact@^0.1:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/cargo-cp-artifact/-/cargo-cp-artifact-0.1.6.tgz#df1bc9dad036ae0f4230639a869182e1d5850f89"
@@ -428,6 +493,21 @@ chalk@^4.0.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+chokidar@^3.5.3:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
 chownr@^1.1.1:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@@ -457,7 +537,7 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-color-name@~1.1.4:
+color-name@^1.1.4, color-name@~1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@@ -528,6 +608,11 @@ cross-spawn@^7.0.2:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
 data-uri-to-buffer@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
@@ -569,6 +654,11 @@ deep-is@^0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
   integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
 
+defined@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf"
+  integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -599,6 +689,20 @@ detect-libc@^1.0.3:
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
 
+detective@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
+  integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
+  dependencies:
+    acorn-node "^1.8.2"
+    defined "^1.0.0"
+    minimist "^1.2.6"
+
+didyoumean@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
+  integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
+
 discord-api-types@^0.18.1:
   version "0.18.1"
   resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.18.1.tgz#5d08ed1263236be9c21a22065d0e6b51f790f492"
@@ -628,6 +732,11 @@ discord.js@^13.1.0:
     node-fetch "^2.6.1"
     ws "^7.5.1"
 
+dlv@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
+  integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
 doctrine@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -861,6 +970,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
+fast-glob@^3.2.12:
+  version "3.2.12"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
+  integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.2"
+    merge2 "^1.3.0"
+    micromatch "^4.0.4"
+
 fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -871,6 +991,13 @@ fast-levenshtein@^2.0.6:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
+fastq@^1.6.0:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
+  integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
+  dependencies:
+    reusify "^1.0.4"
+
 fetch-blob@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.2.tgz#6bc438675f3851ecea51758ac91f6a1cd1bacabd"
@@ -890,6 +1017,13 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
   integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
 
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
 finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -952,6 +1086,16 @@ fs.realpath@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
 functional-red-black-tree@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
@@ -976,13 +1120,20 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
   integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
 
-glob-parent@^5.1.2:
+glob-parent@^5.1.2, glob-parent@~5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
   dependencies:
     is-glob "^4.0.1"
 
+glob-parent@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+  dependencies:
+    is-glob "^4.0.3"
+
 glob@^7.1.3:
   version "7.1.7"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
@@ -1017,6 +1168,13 @@ has-unicode@^2.0.0:
   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
   integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
 
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
 [email protected]:
   version "1.7.2"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
@@ -1105,6 +1263,20 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
   integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.9.0:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
+  integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+  dependencies:
+    has "^1.0.3"
+
 is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1134,6 +1306,18 @@ is-glob@^4.0.0, is-glob@^4.0.1:
   dependencies:
     is-extglob "^2.1.1"
 
+is-glob@^4.0.3, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
 is-obj@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
@@ -1185,6 +1369,11 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+lilconfig@^2.0.5, lilconfig@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
+  integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
+
 lodash.clonedeep@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@@ -1227,11 +1416,24 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
   integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
 
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
 methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
 
+micromatch@^4.0.4, micromatch@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
 [email protected]:
   version "1.49.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
@@ -1266,6 +1468,11 @@ minimist@^1.2.0, minimist@^1.2.3:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
+minimist@^1.2.6:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
+  integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
+
 mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@@ -1302,6 +1509,11 @@ mustache@^4.2.0:
   resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
   integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
 
+nanoid@^3.3.4:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
+  integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+
 napi-build-utils@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
@@ -1340,6 +1552,11 @@ node-fetch@^3.1.0:
     fetch-blob "^3.1.2"
     formdata-polyfill "^4.0.10"
 
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
 npmlog@^4.0.1:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
@@ -1360,6 +1577,11 @@ object-assign@^4.1.0:
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
 
+object-hash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
+  integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
+
 on-finished@~2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -1425,11 +1647,84 @@ path-key@^3.1.0:
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
   integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
 [email protected]:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
   integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
 
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pify@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+  integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
+
+postcss-import@^14.1.0:
+  version "14.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
+  integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
+  dependencies:
+    postcss-value-parser "^4.0.0"
+    read-cache "^1.0.0"
+    resolve "^1.1.7"
+
+postcss-js@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00"
+  integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
+  dependencies:
+    camelcase-css "^2.0.1"
+
+postcss-load-config@^3.1.4:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
+  integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
+  dependencies:
+    lilconfig "^2.0.5"
+    yaml "^1.10.2"
+
[email protected]:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735"
+  integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==
+  dependencies:
+    postcss-selector-parser "^6.0.10"
+
+postcss-selector-parser@^6.0.10:
+  version "6.0.10"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
+  integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.4.17:
+  version "8.4.18"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.18.tgz#6d50046ea7d3d66a85e0e782074e7203bc7fbca2"
+  integrity sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==
+  dependencies:
+    nanoid "^3.3.4"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
 prebuild-install@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.0.tgz#3c5ce3902f1cb9d6de5ae94ca53575e4af0c1574"
@@ -1490,6 +1785,16 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
   integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
 
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quick-lru@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+
 range-parser@~1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -1515,6 +1820,13 @@ rc@^1.2.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+read-cache@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
+  integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
+  dependencies:
+    pify "^2.3.0"
+
 readable-stream@^2.0.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@@ -1537,6 +1849,13 @@ readable-stream@^3.1.1, readable-stream@^3.4.0:
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
 regexpp@^3.1.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
@@ -1552,6 +1871,20 @@ resolve-from@^4.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve@^1.1.7, resolve@^1.22.1:
+  version "1.22.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+  integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+  dependencies:
+    is-core-module "^2.9.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
 rimraf@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -1566,6 +1899,13 @@ rosu-pp@^0.8.0:
   dependencies:
     cargo-cp-artifact "^0.1"
 
+run-parallel@^1.1.9:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+  dependencies:
+    queue-microtask "^1.2.2"
+
 [email protected], safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -1667,6 +2007,11 @@ slice-ansi@^4.0.0:
     astral-regex "^2.0.0"
     is-fullwidth-code-point "^3.0.0"
 
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -1762,6 +2107,11 @@ supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
 table@^6.0.9:
   version "6.7.1"
   resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2"
@@ -1774,6 +2124,35 @@ table@^6.0.9:
     string-width "^4.2.0"
     strip-ansi "^6.0.0"
 
+tailwindcss@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.1.tgz#1bd828fff3172489962357f8d531c184080a6786"
+  integrity sha512-Uw+GVSxp5CM48krnjHObqoOwlCt5Qo6nw1jlCRwfGy68dSYb/LwS9ZFidYGRiM+w6rMawkZiu1mEMAsHYAfoLg==
+  dependencies:
+    arg "^5.0.2"
+    chokidar "^3.5.3"
+    color-name "^1.1.4"
+    detective "^5.2.1"
+    didyoumean "^1.2.2"
+    dlv "^1.1.3"
+    fast-glob "^3.2.12"
+    glob-parent "^6.0.2"
+    is-glob "^4.0.3"
+    lilconfig "^2.0.6"
+    micromatch "^4.0.5"
+    normalize-path "^3.0.0"
+    object-hash "^3.0.0"
+    picocolors "^1.0.0"
+    postcss "^8.4.17"
+    postcss-import "^14.1.0"
+    postcss-js "^4.0.0"
+    postcss-load-config "^3.1.4"
+    postcss-nested "6.0.0"
+    postcss-selector-parser "^6.0.10"
+    postcss-value-parser "^4.2.0"
+    quick-lru "^5.1.1"
+    resolve "^1.22.1"
+
 tar-fs@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
@@ -1800,6 +2179,13 @@ text-table@^0.2.0:
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
 [email protected]:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
@@ -1869,7 +2255,7 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
@@ -1941,7 +2327,17 @@ ws@^7.5.1:
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
   integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
 
+xtend@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
 yallist@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==