Compare commits

...

10 Commits

5 changed files with 220 additions and 98 deletions

View File

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

View File

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

View File

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

View File

@ -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);
});

View File

@ -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;