Merlin-R / User Info Overlay

// ==UserScript==
// @name         User Info Overlay
// @namespace    http://reichwald.me/
// @version      0.5
// @description  Display User Information As Popover
// @author       Merlin Reichwald
// @copyright 2018, Merlin-R (https://openuserjs.org//users/Merlin-R)
// @license MIT
// @match        https://www.torn.com/**
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  class UserOverlay {
    constructor() {
      this.id = '0';
      this.position = [0, 0];
      this.dimensions = [200, 183];
      this.data = {};
      this.apikey = localStorage.apikey;
    }

    setProfile(id) {
      this.id = id;
    }

    placeAt(x, y) {
      this.position = [x, y];
    }

    async show() {
      $('.mrdProfileOverview').remove();
      this.$elem = $(await this.template());
      this.resetTimeout();
      this.$elem.on('mouseover', () => this.resetTimeout());
      this.$elem.on('mousemove', () => this.resetTimeout());
      $('body').append(this.$elem);
    }

    hide() {
      $('.mrdProfileOverview').remove();
    }

    resetTimeout() {
      if (this.tid)
        clearTimeout(this.tid);
      this.tid = setTimeout(() => this.hide(), 1000);
    }

    getClosePos() {
      let [x, y] = this.position;
      let [w, h] = this.dimensions;
      let cw = document.documentElement.clientWidth - 50;
      let ch = document.documentElement.clientHeight - 50;
      let nx, ny;
      if (cw < x + w) nx = x - w;
      else nx = x;
      if (ch < y + h + 16) ny = y - h - 16;
      else ny = y + 16;
      return [nx, ny];
    }

    async fetchProfileData() {
      if (this.data[this.id])
        if (this.data[this.id].fetchedAt > Date.now() - 60000)
          return this.data[this.id];
      if (!this.apikey || this.apikey === 'null') this.promptApiKey();
      let data = await $.get(`https://api.torn.com/user/${this.id}/?key=${this.apikey}&selections=profile,personalstats`);
      data.fetchedAt = Date.now();
      return this.data[this.id] = data;
    }

    promptApiKey() {
      this.apikey = localStorage.apikey = prompt("Api Key");
    }

    formatMoney(money) {
      if (!money || isNaN(+("" + money))) return money;
      money = money.toString();
      let newMoney = "";
      while (money.length > 3) {
        var part = money.substr(money.length - 3);
        newMoney = part + '.' + newMoney;
        money = money.substr(0, money.length - 3);
      }

      newMoney = money + (newMoney ? '.' + newMoney : '');
      return newMoney;
    }

    async template() {
      let [x, y] = this.getClosePos();
      let [w, h] = this.dimensions;
      let id = this.id;
      let data = await this.fetchProfileData();
      return `<div class="mrdProfileOverview" style="
        position: absolute;
        padding: 8px;
        width: ${w}px;
        height: ${h}px;
        background: rgb(242, 242, 242);
        box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 8px;
        border-radius: 3px;
        transition: all        0.3s ease-in-out 0s,
                    visibility 0.3s ease-in-out 0s;
        opacity: 1;
        visibility: visible;
        z-index: 999999;
        top: ${y}px;
        left: ${x}px;">
          <style>
          .mrdProfileOverview a {
            font-weight: 700;
            text-decoration: none;
            color: #333;
          }
          </style>
          <div>
            <b style="display:block">${data.name} [${id}] (${data.level})</b>
            <span>${data.rank}</span>
            <hr />
            <span>${data.status[0]} ${data.status[1]}</span>
            <hr />
            <table style="border-spacing: 4px; border-collapse: separate;">
              <tbody>
                <tr>  <td>Xanax</td>        <td>${data.personalstats.xantaken || "N/A"}</td></tr>
                <tr>  <td>Refills</td>      <td>${data.personalstats.refills  || "N/A"}</td></tr>
                <tr>  <td>Last Active</td>  <td>${data.last_action}</td></tr>
                <tr>  <td>Defends</td>      <td>Won: ${data.personalstats.defendswon || "N/A"} | Lost: ${data.personalstats.defendslost || 0}</td></tr>
                <tr>  <td>Attacks</td>      <td>Won: ${data.personalstats.attackswon || "N/A"} | Lost: ${data.personalstats.attackslost || 0}</td></tr>
                <tr>  <td>Networth</td>     <td>${this.formatMoney(data.personalstats.networth || "N/A")}</td></tr>
              </tbody>
            </table>
            <hr />
            <a href="/loader2.php?sid=getInAttack&user2ID=${id}">Attack</a>
            <a href="/messages.php#/p=compose&XID=${id}">Message</a>
            <a onclick="window.chat.r(${id})">Chat</a>
            <a href="/sendcash.php#/XID=${id}">Send Money</a>
            <a href="/trade.php#step=start&userID=${id}">Trade</a>
            <a href="/bounties.php#/p=add&XID=${id}">Bounty</a>
            <a href="/playerreport.php?step=add&userID=${id}">Report</a>
            <a href="/friendlist.php#/p=add&XID=${id}">Friend</a>
            <a href="/blacklist.php#/p=add&XID=${id}">Enemy</a>
            <a href="/personalstats.php?ID=${id}">Stats</a>
          </div>
        </div>`;
    }

  };

  const overlay = new UserOverlay();
  $(document).on('mouseover', 'a[href*="profiles.php?XID="]', e => {
    console.log(e.pageX, e.pageY, e.target.href)
    var target = e.target;
    while(target.tagName !== 'A') {
      target = target.parentElement;
    }
    var href = target.href;
    var id = href.match(/.*\?XID=([0-9]+).*/)[1];
    var off = $(target).offset();
    overlay.setProfile(id);
    overlay.placeAt(off.left, off.top);
    overlay.show();
  })
})();