Compare commits

9 Commits

14 changed files with 653 additions and 791 deletions

View File

@@ -3,7 +3,7 @@ version: 0.2
phases:
install:
runtime-versions:
nodejs: 18
nodejs: 16
commands:
- echo Installing dependencies...
- npm install

812
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,9 @@
"webpack-cli": "^4.7.2"
},
"dependencies": {
"chart.js": "^4.4.5",
"chartjs-adapter-dayjs-4": "^1.0.4",
"css-minimizer-webpack-plugin": "^7.0.0",
"dayjs": "^1.11.13"
"chart.js": "^4.4.0",
"chartjs-adapter-dayjs-3": "^1.2.3",
"css-minimizer-webpack-plugin": "^5.0.0",
"dayjs": "^1.11.7"
}
}

View File

@@ -1,22 +0,0 @@
export default class Datum {
constructor(time, price) {
this._time = time;
this._price = price;
}
getTime() {
return this._time;
}
getPrice() {
return this._price;
}
getX() {
return this.getTime();
}
getY() {
return this.getPrice();
}
}

View File

@@ -1,4 +0,0 @@
export default async function fetchCurrent() {
const resp = await fetch("https://data.wowtoken.app/classic/token/current.json");
return await resp.json();
}

View File

@@ -1,13 +0,0 @@
import Datum from "./datum";
import urlBuilder from "./urlBuilder";
export default async function fetchData(currentRegionSelection, currentTimeSelection, currentAggregateSelection) {
const data = [];
const resp = await fetch(urlBuilder(currentRegionSelection, currentTimeSelection, currentAggregateSelection));
const respData = await resp.json();
for (let i = 0, l = respData.length; i < l; i++) {
let datum = new Datum(Date.parse(respData[i]['time']), respData[i]['value']);
data.push(datum);
}
return data;
}

View File

@@ -1,18 +0,0 @@
import Datum from "./datum";
function updateHighTime() {
const highTime= document.getElementById("high-time");
const currentTime = document.getElementById("time").selectedOptions[0].innerText;
if (currentTime.toLowerCase() !== highTime.innerText) {
highTime.innerText = currentTime.toLowerCase();
}
}
function updateHighVal(datum) {
const highVal = document.getElementById("high-val");
highVal.innerHTML = datum.getPrice().toLocaleString();
}
export {updateHighTime, updateHighVal};

View File

@@ -1,24 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<title>WoW Classic Historical Token Prices Tracker</title>
<title>WoW Historical Token Prices Tracker</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Track current and historical gold price trends for the World of Warcraft (WoW) Classic in game token, including the US, EU, TW, and KR regions. Prices updated every minute. Simple, quick, and easy info, no ads or tracking, ever.">
<meta name="description" content="Track current and historical gold price trends for the World of Warcraft (WoW) in game token, including the US, EU, TW, and KR regions. Prices updated every minute. Simple, quick, and easy info, no ads or tracking, ever.">
<link rel="preconnect" href="https://data.wowtoken.app">
<link rel="dns-prefetch" href="https://data.wowtoken.app">
<link rel="preload" href="https://data.wowtoken.app/classic/token/current.json" as="fetch" type="application/json" crossorigin="anonymous">
<link rel="preload" href="https://data.wowtoken.app/classic/token/history/us/72h.json" as="fetch" type="application/json" crossorigin="anonymous">
<link rel="preload" href="https://data.wowtoken.app/token/current.json" as="fetch" type="application/json" crossorigin="anonymous">
<link rel="preload" href="https://data.wowtoken.app/token/history/us/72h.json" as="fetch" type="application/json" crossorigin="anonymous">
</head>
<body>
<div class="flex-container">
<div class="data-header">
<h1>1 Classic Token = <u id="token">0</u> Gold</h1>
<div class="high-low">
<p>Lowest in last <em id="low-time">3 days</em>: <u id="low-val">0</u></p>
<p>Highest in last <em id="high-time">3 days</em>: <u id="high-val">0</u></p>
</div>
</div>
<div><h1>1 Token = <u id="token">0</u> Gold</h1></div>
<div id="chart-frame">
<div class="lds-ripple" id="loader"><div></div><div></div></div>
<canvas id="token-chart"></canvas>
@@ -42,6 +36,9 @@
<option value="336h">14 Days</option>
<option value="720h">1 Month</option>
<option value="90d">3 Months</option>
<option value="6m">6 Months</option>
<option value="1y">1 Year</option>
<option value="2y">2 Years</option>
<option value="all">All Available</option>
</select>
</div>
@@ -52,11 +49,23 @@
</fieldset>
<fieldset id="advanced-options">
<legend>Advanced chart options</legend>
<fieldset id="advanced-region-options">
<legend>Multi-Region Selection</legend>
<label for="adv-r-us">US:</label>
<input type="checkbox" id="adv-r-us" name="adv-r-us" value="enable" disabled />
<label for="adv-r-eu">EU:</label>
<input type="checkbox" id="adv-r-eu" name="adv-r-eu" value="enable" disabled />
<label for="adv-r-kr">KR:</label>
<input type="checkbox" id="adv-r-kr" name="adv-r-kr" value="enable" disabled />
<label for="adv-r-tw">TW:</label>
<input type="checkbox" id="adv-r-tw" name="adv-r-tw" value="enable" disabled/>
</fieldset>
<fieldset id="basic-smoothing">
<label for="aggregate">Smoothing Function:</label>
<select name="aggregate" id="aggregate">
<option id='agg_none' value="none">None</option>
<option id='agg_davg' value="daily_mean">Daily Average</option>
<option id='agg_wavg' value="weekly_mean" disabled>Weekly Average</option>
</select>
</fieldset>
<fieldset id="y-start-options">
@@ -72,12 +81,12 @@
</button>
</div>
<div>
<p><em>Looking for the retail WoW Token price? Find it <a href="https://wowtoken.app">here!</a></em></p>
<p><em>Looking for the classic WoW Token price? Find it <a href="https://classic.wowtoken.app">here!</a></em></p>
</div>
</div>
<details id="about">
<summary>About this Site</summary>
This is a site developed to track the value of the World of Warcraft Classic Token from various
This is a site developed to track the value of the World of Warcraft Token from various
regions over time. I developed it because I wanted a quick and simple way to track the
cost without being advertised to or tracked, and to play around with various "serverless"
technologies. As such, my promise to you is never to use any tracking Javascript, and the
@@ -85,10 +94,10 @@
</details>
<details id="what-is">
<summary>What is the WoW Token</summary>
The World of Warcraft Classic Token is a first-party system developed by Blizzard to allow you
The World of Warcraft Token is a first-party system developed by Blizzard to allow you
to either spend currency (local denomination or Blizzard Balance) and convert it to gold
in classic World of Warcraft, or use gold to buy game time.
To find out more, visit the support article on Blizzard's website
in retail World of Warcraft, or use gold to buy game time or Blizzard Balance. To find out
more, visit the support article on Blizzard's website
<a href="https://us.battle.net/support/en/article/31218">here</a>.
</details>
<div id="source">

View File

@@ -1,94 +1,230 @@
import 'chartjs-adapter-dayjs-4';
import {
Chart,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
TimeSeriesScale,
Title,
Tooltip
} from 'chart.js';
import 'chartjs-adapter-dayjs-3';
import "./style.css"
import fetchCurrent from "./fetchCurrent";
import fetchData from "./fetchData";
import {updateHighTime} from "./highTime";
import {updateLowTime} from "./lowTime";
import {addLoader, removeLoader} from "./loader";
import TokenChart from "./tokenChart";
import Datum from "./datum";
// TODO: This file should be seperated into multiple with better ownership
Chart.register(
LineElement,
PointElement,
LineController,
LinearScale,
TimeSeriesScale,
Legend,
Title,
Tooltip
)
let currentRegionSelection = '';
let currentTimeSelection = '';
let currentAggregateSelection = '';
let startYAtZero = false;
let chart;
const currentPriceHash = {
us: 0,
eu: 0,
kr: 0,
tw: 0
};
const chartData = {
let chartData = {
us: [],
eu: [],
kr: [],
tw: []
}
let chartOptions = {
us: {
color: 'gold'
},
eu: {
color: 'red'
},
kr: {
color: 'white'
},
tw: {
color: 'pink'
}
}
let chartJsData;
let ctx;
let tokenChart;
function populateChart() {
ctx = document.getElementById("token-chart").getContext('2d');
tokenChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
borderColor: 'gold',
label: currentRegionSelection.toUpperCase() + " WoW Token Price",
data: chartJsData,
cubicInterpolationMode: 'monotone',
pointRadius: 0
}]
},
options: {
interaction: {
intersect: false,
mode: "index"
},
scales: {
x: {
type: 'time',
grid: {
color: '#625f62',
},
ticks: {
color: '#a7a4ab',
font: {
size: 18,
}
},
time: {
unit: lookupTimeUnit(currentTimeSelection)
}
},
y: {
beginAtZero: startYAtZero,
grid: {
color: '#2f2c2f',
},
ticks: {
color: '#a7a4ab',
font: {
size: 18,
}
}
}
},
}
});
}
function lookupTimeUnit(query){
const lookup = {
'h': 'day',
'd': 'week',
'm': 'month',
'y': 'month',
'l': 'year'
}
return lookup[query.charAt(query.length - 1)]
}
async function callUpdateURL() {
await updateTokens(await fetchCurrent());
let resp = await fetch("https://data.wowtoken.app/token/current.json");
let data = await resp.json();
updateTokens(data);
}
async function updateTokens(data) {
await Promise.all([
updateRegionalToken('us', data),
updateRegionalToken('eu', data),
updateRegionalToken('kr', data),
updateRegionalToken('tw', data)
]);
function updateTokens(data) {
updateRegionalToken('us', data);
updateRegionalToken('eu', data);
updateRegionalToken('kr', data);
updateRegionalToken('tw', data);
}
async function updateRegionalToken(region, data) {
function updateRegionalToken(region, data) {
if (currentPriceHash[region] !== data['price_data'][region]) {
currentPriceHash[region] = data['price_data'][region];
if (region === currentRegionSelection) {
formatToken();
const datum = new Datum(Date.parse(data['current_time']), data['price_data'][region]);
if (currentAggregateSelection === 'none' && chart.active()) {
await chart.addDataToChart(datum);
}
else if (currentAggregateSelection === 'none' && !chart.active()) {
await chart.lateUpdate(datum);
if (currentAggregateSelection === 'none') {
addDataToChart(region, data);
}
}
}
}
async function updateRegionPreference(newRegion) {
function addDataToChart(region, data) {
if (tokenChart) {
const datum = {x: data['current_time'], y: data['price_data'][region]}
tokenChart.data.datasets.forEach((dataset) => {
dataset.data.push(datum);
})
tokenChart.update();
}
}
async function aggregateFunctionToggle() {
// TODO: We should probably make these global or something
// so if the need to be updated in the future we can do so easily
const smallTimes = ['72h', '168h', '336h'];
const longTimes = ['720h', '30d', '2190h', '90d', '1y', '2y', '6m', 'all'];
const idsToModify = ['agg_wavg']
if (smallTimes.includes(currentTimeSelection)) {
for (const id of idsToModify) {
let ele = document.getElementById(id);
ele.disabled = true;
}
} else if (longTimes.includes(currentTimeSelection)) {
for (const id of idsToModify) {
let ele = document.getElementById(id);
ele.disabled = false;
}
}
}
function addLoader() {
let loader = document.getElementById('loader');
if (!loader) {
const blank_div = document.createElement('div');
let loaderNode = blank_div.cloneNode();
loaderNode.id = 'loader';
loaderNode.className = 'lds-ripple';
loaderNode.appendChild(blank_div.cloneNode());
loaderNode.appendChild(blank_div.cloneNode());
let chartNode = document.getElementById('token-chart');
chartNode.before(loaderNode);
}
}
function removeLoader () {
let loader = document.getElementById('loader');
if (loader) {
loader.remove();
}
}
function updateRegionPreference(newRegion) {
if (newRegion !== currentRegionSelection) {
await chart.destroyChart();
tokenChart.destroy();
addLoader();
currentRegionSelection = newRegion;
}
formatToken();
chart = new TokenChart();
await pullChartData();
pullChartData().then(populateChart);
}
async function updateTimePreference(newTime) {
function updateTimePreference(newTime) {
if (newTime !== currentTimeSelection) {
await chart.destroyChart();
tokenChart.destroy();
addLoader();
currentTimeSelection = newTime;
aggregateFunctionToggle();
}
chart = new TokenChart();
await pullChartData();
updateHighTime();
updateLowTime();
pullChartData().then(populateChart);
}
async function updateAggregatePreference(newAggregate) {
function updateAggregatePreference(newAggregate) {
if (newAggregate !== currentAggregateSelection) {
await chart.destroyChart();
tokenChart.destroy();
addLoader();
currentAggregateSelection = newAggregate;
}
chart = new TokenChart();
await pullChartData();
pullChartData().then(populateChart);
}
function toggleAdvancedSetting() {
@@ -105,20 +241,33 @@ function toggleAdvancedSetting() {
function toggleStartYAtZero(){
startYAtZero = document.getElementById('y-start').checked;
chart.toggleYStart(startYAtZero);
if (tokenChart){
tokenChart.options.scales.y.beginAtZero = startYAtZero;
tokenChart.update();
}
}
function urlBuilder() {
let url = "https://data.wowtoken.app/token/history/";
if (currentAggregateSelection !== 'none') {
url += `${currentAggregateSelection}/`
}
url += `${currentRegionSelection}/${currentTimeSelection}.json`
return url;
}
async function pullChartData() {
chartData[currentRegionSelection] = await fetchData(currentRegionSelection, currentTimeSelection, currentAggregateSelection);
if (!chart.active()) {
await chart.createChart(currentRegionSelection, currentTimeSelection, startYAtZero, chartData[currentRegionSelection]);
}
else {
for (let i = 0; i < chartData[currentRegionSelection].length; i++) {
await chart.addDataToChart(chartData[currentRegionSelection][i]);
console.warn("This should never hit, and should be okay to remove");
}
let resp = await fetch(urlBuilder());
let chartData = await resp.json();
let newChartJSData = [];
for (let i = 0; i < chartData.length; i++) {
let datum = {
x: chartData[i]['time'],
y: chartData[i]['value']
};
newChartJSData.push(datum);
}
chartJsData = newChartJSData;
removeLoader();
}
@@ -156,15 +305,13 @@ function detectTimeQuery(urlSearchParams) {
timeDDL.options[i].selected = true;
}
}
updateHighTime();
updateLowTime();
} else {
console.warn("An incorrect or malformed time selection was made in the query string");
}
}
function detectAggregateQuery(urlSearchParams) {
const validOperations = ['none', 'daily_mean'];
const validOperations = ['none', 'daily_mean', 'weekly_mean'];
if (validOperations.includes(urlSearchParams.get('aggregate').toLowerCase())) {
currentAggregateSelection = urlSearchParams.get('aggregate').toLowerCase();
let aggregateDDL = document.getElementById('aggregate');
@@ -173,6 +320,7 @@ function detectAggregateQuery(urlSearchParams) {
aggregateDDL.options[i].selected = true;
}
}
aggregateFunctionToggle();
} else {
console.warn("An incorrect or malformed aggregate selection was made in the query string");
}
@@ -205,7 +353,7 @@ function detectURLQuery() {
}
function buildDeepLinksURL() {
let url = "https://classic.wowtoken.app/?"
let url = "https://wowtoken.app/?"
if (currentTimeSelection !== '72h'){
url += `time=${currentTimeSelection}&`
}
@@ -280,11 +428,7 @@ function registerOptionHandlers() {
document.addEventListener('DOMContentLoaded', function () {
registerEventHandles();
detectURLQuery();
chart = new TokenChart();
Promise.all([
callUpdateURL(),
]).then(pullChartData);
Promise.all([callUpdateURL(), pullChartData()]).then(populateChart)
setInterval(callUpdateURL, 60*1000);
}, false);

View File

@@ -1,22 +0,0 @@
function addLoader() {
let loader = document.getElementById('loader');
if (!loader) {
const blank_div = document.createElement('div');
let loaderNode = blank_div.cloneNode();
loaderNode.id = 'loader';
loaderNode.className = 'lds-ripple';
loaderNode.appendChild(blank_div.cloneNode());
loaderNode.appendChild(blank_div.cloneNode());
let chartNode = document.getElementById('token-chart');
chartNode.before(loaderNode);
}
}
function removeLoader () {
let loader = document.getElementById('loader');
if (loader) {
loader.remove();
}
}
export {addLoader, removeLoader};

View File

@@ -1,17 +0,0 @@
import Datum from "./datum";
function updateLowTime() {
const lowTime= document.getElementById("low-time");
const currentTime = document.getElementById("time").selectedOptions[0].innerText;
if (currentTime.toLowerCase() !== lowTime.innerText) {
lowTime.innerText = currentTime.toLowerCase();
}
}
function updateLowVal(datum) {
const lowVal = document.getElementById("low-val");
lowVal.innerHTML = datum.getPrice().toLocaleString();
}
export {updateLowTime, updateLowVal};

View File

@@ -173,8 +173,8 @@ h6 {
font-weight: 700;
}
html {
background-color: #6b4233;
color: #b7b7b7;
background-color: #073642;
color: #839496;
margin: 1em;
}
/*body {
@@ -194,7 +194,7 @@ a:hover {
color: #cb4b16;
}
h1 {
color: #ffd5e9;
color: #d33682;
}
h2,
h3,
@@ -206,7 +206,7 @@ h6 {
pre {
background-color: #002b36;
color: #839496;
border: 1pt solid #000000;
border: 1pt solid #586e75;
padding: 1em;
box-shadow: 5pt 5pt 8pt #073642;
}
@@ -301,10 +301,10 @@ h6 {
display: flex;
flex-direction: column;
justify-content: center;
background-color: #2f201e;
background-color: #002b36;
margin: 0 auto;
max-width: 85%;
border: 1pt solid #000000;
border: 1pt solid #586e75;
padding: 1em;
}
@@ -341,8 +341,8 @@ p {
}
details {
background-color: #6b4233;
border: 1px solid #000000;
background-color: #073642;
border: 1px solid #aaa;
border-radius: 4px;
padding: 0.5em 0.5em 0;
font-size: 17px;
@@ -363,11 +363,10 @@ details[open] {
}
details[open] summary {
border-bottom: 1px solid #000000;
border-bottom: 1px solid #aaa;
margin-bottom: 0.5em;
}
#option_select {
font-size: 20px;
line-height: 40px;
@@ -420,21 +419,6 @@ details[open] summary {
margin: 10px;
}
.data-header h1 {
margin-top: 24px;
margin-bottom: 24px;
}
.high-low {
display: flex;
justify-content: space-evenly;
}
.high-low p {
line-height: 1em;
font-size: 24px;
}
.lds-ripple {
position: relative;
align-self: center;

View File

@@ -1,181 +0,0 @@
import {
Chart,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
TimeSeriesScale,
Title,
Tooltip
} from 'chart.js';
import 'chartjs-adapter-dayjs-4';
Chart.register(
LineElement,
PointElement,
LineController,
LinearScale,
TimeSeriesScale,
Legend,
Title,
Tooltip
)
import {updateHighVal} from "./highTime";
import {updateLowVal} from "./lowTime";
function lookupTimeUnit(query){
const lookup = {
'h': 'day',
'd': 'week',
'm': 'month',
'y': 'month',
'l': 'year'
}
return lookup[query.charAt(query.length - 1)]
}
export default class TokenChart {
constructor() {
this._context = document.getElementById("token-chart").getContext('2d');
this._chartActive = false;
this._lastDatum = null;
this._highDatum = null;
this._lowDatum = null;
this._lateUpdate = false;
}
get highDatum() {
return this._highDatum;
}
get lowDatum() {
return this._lowDatum;
}
async createChart(region, time, yLevel, data) {
const chartData = [];
let lateUpdateData = this._lastDatum;
for (let i = 0; i < data.length; i++) {
this._lastDatum = data[i];
if (this._highDatum === null || this._lastDatum.getPrice() > this._highDatum.getPrice()) {
this._highDatum = data[i];
}
if (this._lowDatum === null || this._lowDatum.getPrice() > this._lastDatum.getPrice()) {
this._lowDatum = data[i];
}
chartData.push({
x: data[i].getX(),
y: data[i].getY(),
})
}
updateHighVal(this.highDatum);
updateLowVal(this.lowDatum);
this._chart = new Chart(this._context, {
type: 'line',
data: {
datasets: [{
borderColor: 'gold',
label: region.toUpperCase() + " WoW Token Price",
data: chartData,
cubicInterpolationMode: 'monotone',
pointRadius: 0
}]
},
options: {
interaction: {
intersect: false,
mode: "index"
},
scales: {
x: {
type: 'time',
grid: {
color: '#625f62',
},
ticks: {
color: '#a7a4ab',
font: {
size: 18,
}
},
time: {
unit: lookupTimeUnit(time)
}
},
y: {
beginAtZero: yLevel,
grid: {
color: '#2f2c2f',
},
ticks: {
color: '#a7a4ab',
font: {
size: 18,
}
}
}
},
}
});
if (this._lateUpdate) {
if (this._lastDatum.getPrice() !== lateUpdateData.getPrice() &&
this._lastDatum.getTime() < lateUpdateData.getTime()) {
await this.addDataToChart(lateUpdateData);
}
this._lateUpdate = false
}
this._chartActive = true;
}
async destroyChart() {
await this._chart.destroy();
this._chartActive = false;
this._lastDatum = null;
this._highDatum = null;
this._lowDatum = null;
this._lateUpdate = false;
}
async lateUpdate(datum){
this._lastDatum = datum;
this._lateUpdate = true;
}
async addDataToChart(datum) {
this._lastDatum = datum;
if (datum.getPrice() > this._highDatum.getPrice()) {
this._highDatum = datum;
updateHighVal(this.highDatum);
}
else if (datum.getPrice() < this._lowDatum.getPrice()) {
this._lowDatum = datum;
updateLowVal(this.lowDatum);
}
this._chart.data.datasets.forEach((dataset) => {
dataset.data.push({
x: datum.getX(),
y: datum.getY(),
})
});
this._chart.update();
}
active() {
return this._chartActive;
}
toggleYStart(startYAtZero) {
this._chart.options.scales.y.beginAtZero = startYAtZero;
this._chart.update();
}
}

View File

@@ -1,8 +0,0 @@
export default function urlBuilder(currentRegionSelection, currentTimeSelection, currentAggregateSelection) {
let url = "https://data.wowtoken.app/classic/token/history/";
if (currentAggregateSelection !== 'none') {
url += `${currentAggregateSelection}/`
}
url += `${currentRegionSelection}/${currentTimeSelection}.json`
return url;
}