Compare commits
10 Commits
30f72f91ac
...
69cd58e487
Author | SHA1 | Date | |
---|---|---|---|
69cd58e487 | |||
4ce5481fd0 | |||
25e9353f22 | |||
543b9ba9d2 | |||
71b38778f1 | |||
6379803a45 | |||
11a3fd7fff | |||
e0b6ea9f8e | |||
b92f24739b | |||
23ca3cbcef |
@ -3,7 +3,7 @@ version: 0.2
|
||||
phases:
|
||||
install:
|
||||
runtime-versions:
|
||||
nodejs: 14
|
||||
nodejs: 16
|
||||
commands:
|
||||
- echo Installing dependencies...
|
||||
- npm install
|
||||
|
@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack && mkdir -p dist && cp src/robots.txt src/index.html src/style.css src/favicon.ico dist/"
|
||||
"build": "webpack --mode production && mkdir -p dist && cp src/robots.txt src/index.html src/style.css src/favicon.ico dist/",
|
||||
"build-dev": "webpack --mode development && mkdir -p dist && cp src/robots.txt src/index.html src/style.css src/favicon.ico dist/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "^5.73.0",
|
||||
|
@ -19,32 +19,61 @@
|
||||
<div class="lds-ripple" id="loader"><div></div><div></div></div>
|
||||
<canvas id="token-chart"></canvas>
|
||||
<div id="option_select">
|
||||
<label for="region">Region:</label>
|
||||
<select name="region" id="region">
|
||||
<option value="us">US</option>
|
||||
<option value="eu">EU</option>
|
||||
<option value="kr">KR</option>
|
||||
<option value="tw">TW</option>
|
||||
</select>
|
||||
<br />
|
||||
<label for="time">Time Selection:</label>
|
||||
<select name="time" id="time">
|
||||
<option value="72h">3 Days</option>
|
||||
<option value="168h">7 Days</option>
|
||||
<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>
|
||||
<p>
|
||||
<label for="region">Region:</label>
|
||||
<select name="region" id="region">
|
||||
<option value="us">US</option>
|
||||
<option value="eu">EU</option>
|
||||
<option value="kr">KR</option>
|
||||
<option value="tw">TW</option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="time">Time Selection:</label>
|
||||
<select name="time" id="time">
|
||||
<option value="72h">3 Days</option>
|
||||
<option value="168h">7 Days</option>
|
||||
<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>
|
||||
</p>
|
||||
<p>
|
||||
<label for="aggregate">Aggregate Function:</label>
|
||||
<select name="aggregate" id="aggregate">
|
||||
<option id='agg_none' value="none">None</option>
|
||||
<option id='agg_dmax' value="daily_max">Daily Maximum</option>
|
||||
<option id='agg_dmin' value="daily_min">Daily Minimum</option>
|
||||
<option id='agg_davg' value="daily_mean">Daily Average</option>
|
||||
<option id='agg_wmax' value="weekly_max" disabled>Weekly Maximum</option>
|
||||
<option id='agg_wmin' value="weekly_min" disabled>Weekly Minimum</option>
|
||||
<option id='agg_wavg' value="weekly_mean" disabled>Weekly Average</option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
<details id="about">
|
||||
<summary>About this Site</summary>
|
||||
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
|
||||
only logging is for debugging purposes of the backend - which does not get IPs.
|
||||
</details>
|
||||
<details id="what-is">
|
||||
<summary>What is the WoW Token</summary>
|
||||
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 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">
|
||||
<p>
|
||||
<a href="https://github.com/sneaky-emily/wowtoken.app">Source</a>
|
||||
|
|
||||
<a href="https://blog.emily.sh/2021/04/developing-a-simple-wow-token-tracker/">About</a>
|
||||
<a href="https://blog.emily.sh/wowtoken-app/">Source</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
206
src/index.js
206
src/index.js
@ -1,11 +1,11 @@
|
||||
import {
|
||||
Chart,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LineController,
|
||||
LinearScale,
|
||||
TimeSeriesScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip
|
||||
} from 'chart.js';
|
||||
@ -24,28 +24,29 @@ Chart.register(
|
||||
Tooltip
|
||||
)
|
||||
|
||||
let current_region_selection = ''
|
||||
let current_time_selection = ''
|
||||
const current_price_hash = {
|
||||
let currentRegionSelection = '';
|
||||
let currentTimeSelection = '';
|
||||
let currentAggregateSelection = '';
|
||||
const currentPriceHash = {
|
||||
us: 0,
|
||||
eu: 0,
|
||||
kr: 0,
|
||||
tw: 0
|
||||
}
|
||||
let chart_js_data;
|
||||
};
|
||||
let chartJsData;
|
||||
let ctx;
|
||||
let token_chart;
|
||||
let tokenChart;
|
||||
|
||||
|
||||
function populateChart() {
|
||||
ctx = document.getElementById("token-chart").getContext('2d');
|
||||
token_chart = new Chart(ctx, {
|
||||
tokenChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
borderColor: 'gold',
|
||||
label: current_region_selection.toUpperCase() + " WoW Token Price",
|
||||
data: chart_js_data,
|
||||
label: currentRegionSelection.toUpperCase() + " WoW Token Price",
|
||||
data: chartJsData,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
pointRadius: 0
|
||||
}]
|
||||
@ -79,22 +80,43 @@ function updateTokens(data) {
|
||||
}
|
||||
|
||||
function updateRegionalToken(region, data) {
|
||||
if (current_price_hash[region] !== data['price_data'][region]) {
|
||||
current_price_hash[region] = data['price_data'][region];
|
||||
if (region === current_region_selection) {
|
||||
if (currentPriceHash[region] !== data['price_data'][region]) {
|
||||
currentPriceHash[region] = data['price_data'][region];
|
||||
if (region === currentRegionSelection) {
|
||||
formatToken();
|
||||
add_data_to_chart(region, data);
|
||||
if (currentAggregateSelection === 'none') {
|
||||
addDataToChart(region, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function add_data_to_chart(region, data) {
|
||||
if (token_chart) {
|
||||
function addDataToChart(region, data) {
|
||||
if (tokenChart) {
|
||||
const datum = {x: data['current_time'], y: data['price_data'][region]}
|
||||
token_chart.data.datasets.forEach((dataset) => {
|
||||
tokenChart.data.datasets.forEach((dataset) => {
|
||||
dataset.data.push(datum);
|
||||
})
|
||||
token_chart.update();
|
||||
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_wmax', 'agg_wmin', '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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,97 +141,141 @@ function removeLoader () {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateRegionPreference(newRegion) {
|
||||
if (newRegion !== current_region_selection) {
|
||||
token_chart.destroy();
|
||||
function updateRegionPreference(newRegion) {
|
||||
if (newRegion !== currentRegionSelection) {
|
||||
tokenChart.destroy();
|
||||
addLoader();
|
||||
current_region_selection = newRegion;
|
||||
currentRegionSelection = newRegion;
|
||||
}
|
||||
formatToken();
|
||||
pullChartData().then(populateChart);
|
||||
}
|
||||
|
||||
export function updateTimePreference(newTime) {
|
||||
if (newTime !== current_time_selection) {
|
||||
token_chart.destroy();
|
||||
function updateTimePreference(newTime) {
|
||||
if (newTime !== currentTimeSelection) {
|
||||
tokenChart.destroy();
|
||||
addLoader();
|
||||
current_time_selection = newTime;
|
||||
currentTimeSelection = newTime;
|
||||
aggregateFunctionToggle();
|
||||
}
|
||||
pullChartData().then(populateChart);
|
||||
}
|
||||
|
||||
function updateAggregatePreference(newAggregate) {
|
||||
if (newAggregate !== currentAggregateSelection) {
|
||||
tokenChart.destroy();
|
||||
addLoader();
|
||||
currentAggregateSelection = newAggregate;
|
||||
}
|
||||
pullChartData().then(populateChart);
|
||||
}
|
||||
|
||||
function urlBuilder() {
|
||||
let url = "https://data.wowtoken.app/token/history/";
|
||||
if (currentAggregateSelection !== 'none') {
|
||||
url += `${currentAggregateSelection}/`
|
||||
}
|
||||
url += `${currentRegionSelection}/${currentTimeSelection}.json`
|
||||
return url;
|
||||
}
|
||||
|
||||
async function pullChartData() {
|
||||
let resp = await fetch("https://data.wowtoken.app/token/history/" + current_region_selection + "/" + current_time_selection + ".json");
|
||||
let chart_data = await resp.json();
|
||||
let new_chart_js_data = [];
|
||||
for (let i = 0; i < chart_data.length; i++) {
|
||||
let resp = await fetch(urlBuilder());
|
||||
let chartData = await resp.json();
|
||||
let newChartJSData = [];
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
let datum = {
|
||||
x: chart_data[i]['time'],
|
||||
y: chart_data[i]['value']
|
||||
x: chartData[i]['time'],
|
||||
y: chartData[i]['value']
|
||||
};
|
||||
new_chart_js_data.push(datum);
|
||||
newChartJSData.push(datum);
|
||||
}
|
||||
chart_js_data = new_chart_js_data;
|
||||
chartJsData = newChartJSData;
|
||||
removeLoader();
|
||||
}
|
||||
|
||||
async function updateChartData() {
|
||||
token_chart.destroy();
|
||||
pullChartData().then(populateChart);
|
||||
}
|
||||
|
||||
function formatToken() {
|
||||
$("#token").html(current_price_hash[current_region_selection].toLocaleString());
|
||||
$("#token").html(currentPriceHash[currentRegionSelection].toLocaleString());
|
||||
}
|
||||
|
||||
function detectURLQuery() {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const allowedRegions = ['us', 'eu', 'tw', 'kr']
|
||||
if (urlSearchParams.has('region')) {
|
||||
if (allowedRegions.includes(urlSearchParams.get('region').toLowerCase())) {
|
||||
current_region_selection = urlSearchParams.get('region').toLowerCase()
|
||||
let region_ddl = document.getElementById('region')
|
||||
for (let i = 0; i < region_ddl.options.length; i++){
|
||||
if (region_ddl.options[i].value === current_region_selection) {
|
||||
region_ddl.options[i].selected = true;
|
||||
}
|
||||
// TODO: These maybe able to be collapsed into a single function with params or a lambda
|
||||
|
||||
function detectRegionQuery(urlSearchParams) {
|
||||
const validRegions = ['us', 'eu', 'tw', 'kr'];
|
||||
if (validRegions.includes(urlSearchParams.get('region').toLowerCase())) {
|
||||
let regionDDL = document.getElementById('region');
|
||||
for (let i = 0; i < regionDDL.options.length; i++) {
|
||||
if (regionDDL.options[i].value === currentRegionSelection) {
|
||||
regionDDL.options[i].selected = true;
|
||||
}
|
||||
} else {
|
||||
console.log("An incorrect or malformed region selection was made in the query string")
|
||||
}
|
||||
} else {
|
||||
console.log("An incorrect or malformed region selection was made in the query string");
|
||||
}
|
||||
}
|
||||
|
||||
function detectTimeQuery(urlSearchParams) {
|
||||
// In the future, we will allow all the times to be selected,
|
||||
// once I come up with a good reduction algorithm.
|
||||
// For larger time selections, it's currently hardcoded into the backend
|
||||
const validTimes = ['72h', '168h', '336h', '720h', '30d', '90d', '1y', '2y', '6m', 'all'];
|
||||
if (urlSearchParams.has('time')) {
|
||||
if (validTimes.includes(urlSearchParams.get('time').toLowerCase())) {
|
||||
current_time_selection = urlSearchParams.get('time').toLowerCase();
|
||||
let time_ddl = document.getElementById('time')
|
||||
for (let i = 0; i < time_ddl.options.length; i++){
|
||||
if (time_ddl.options[i].value === current_time_selection) {
|
||||
time_ddl.options[i].selected = true;
|
||||
}
|
||||
const validTimes = ['72h', '168h', '336h', '720h', '30d', '2190h', '90d', '1y', '2y', '6m', 'all'];
|
||||
if (validTimes.includes(urlSearchParams.get('time').toLowerCase())) {
|
||||
currentTimeSelection = urlSearchParams.get('time').toLowerCase();
|
||||
let timeDDL = document.getElementById('time');
|
||||
for (let i = 0; i < timeDDL.options.length; i++) {
|
||||
if (timeDDL.options[i].value === currentTimeSelection) {
|
||||
timeDDL.options[i].selected = true;
|
||||
}
|
||||
} else {
|
||||
console.log("An incorrect or malformed time selection was made in the query string");
|
||||
}
|
||||
} else {
|
||||
console.log("An incorrect or malformed time selection was made in the query string");
|
||||
}
|
||||
}
|
||||
|
||||
function detectAggregateQuery(urlSearchParams) {
|
||||
const validOperations = ['none', 'daily_max', 'daily_min', 'daily_mean', 'weekly_max', 'weekly_min', 'weekly_mean'];
|
||||
if (validOperations.includes(urlSearchParams.get('aggregate').toLowerCase())) {
|
||||
currentAggregateSelection = urlSearchParams.get('aggregate').toLowerCase();
|
||||
let aggregateDDL = document.getElementById('aggregate');
|
||||
for (let i = 0; i < aggregateDDL.options.length; i++) {
|
||||
if (aggregateDDL.options[i].value === currentAggregateSelection) {
|
||||
aggregateDDL.options[i].selected = true;
|
||||
}
|
||||
}
|
||||
aggregateFunctionToggle();
|
||||
} else {
|
||||
console.log("An incorrect or malformed aggregate selection was made in the query string");
|
||||
}
|
||||
}
|
||||
|
||||
function detectURLQuery() {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
if (urlSearchParams.has('region')) {
|
||||
detectRegionQuery(urlSearchParams);
|
||||
}
|
||||
if (urlSearchParams.has('time')) {
|
||||
detectTimeQuery(urlSearchParams);
|
||||
}
|
||||
if (urlSearchParams.has('aggregate')) {
|
||||
detectAggregateQuery(urlSearchParams);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
document.getElementById('region').addEventListener('change', function() {
|
||||
updateRegionPreference(this.value);
|
||||
});
|
||||
current_region_selection = document.getElementById('region').value;
|
||||
currentRegionSelection = document.getElementById('region').value;
|
||||
document.getElementById('time').addEventListener('change', function() {
|
||||
updateTimePreference(this.value);
|
||||
});
|
||||
current_time_selection = document.getElementById('time').value;
|
||||
currentTimeSelection = document.getElementById('time').value;
|
||||
document.getElementById('aggregate').addEventListener('change', function () {
|
||||
updateAggregatePreference(this.value);
|
||||
})
|
||||
currentAggregateSelection = document.getElementById('aggregate').value;
|
||||
detectURLQuery();
|
||||
callUpdateURL();
|
||||
Promise.all([callUpdateURL(), pullChartData()]).then(populateChart)
|
||||
setInterval(callUpdateURL, 60*1000);
|
||||
pullChartData().then(populateChart);
|
||||
});
|
||||
|
||||
|
@ -181,7 +181,7 @@ html {
|
||||
margin: 1em;
|
||||
}
|
||||
/*body {
|
||||
|
||||
|
||||
}*/
|
||||
code {
|
||||
background-color: #073642;
|
||||
@ -309,7 +309,7 @@ h6 {
|
||||
max-width: 85%;
|
||||
border: 1pt solid #586e75;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-container > div {
|
||||
flex: 100%;
|
||||
@ -324,6 +324,32 @@ p {
|
||||
line-height: 3em;
|
||||
}
|
||||
|
||||
details {
|
||||
background-color: #073642;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
padding: 0.5em 0.5em 0;
|
||||
font-size: 17px;
|
||||
line-height: 1.5em;
|
||||
margin: .5em 5vw;
|
||||
}
|
||||
|
||||
details > summary {
|
||||
color: #aaaaaa;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin: -0.5em -0.5em 0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
details[open] summary {
|
||||
border-bottom: 1px solid #aaa;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#option_select {
|
||||
font-size: 20px;
|
||||
@ -352,12 +378,12 @@ p {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.lds-ripple {
|
||||
position: relative;
|
||||
align-self: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: -80px;
|
||||
}
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
|
Loading…
Reference in New Issue
Block a user