Compare commits

10 Commits
Author SHA1 Message Date
emily 69cd58e487 Bump node build version to match new AWS Builder 2023-03-04 15:50:48 -08:00
emily 4ce5481fd0 Correct build rule to include production mode
Not that it wasn't building in production before, but this means it no longer warns it's using the default values
2023-03-04 15:48:02 -08:00
emily 25e9353f22 Add aggregate to URL params and bug fixes 2023-03-04 15:47:06 -08:00
emily 543b9ba9d2 Added new aggregate functions feature 2023-02-19 17:44:59 -08:00
emily 71b38778f1 Added 'details' sections to provide more information about the site 2023-02-12 03:01:03 -08:00
emily 6379803a45 Made the external data requests parallel 2023-02-09 00:17:35 -08:00
emily 11a3fd7fff Adjusted selection dropdowns to be more compact 2023-02-09 00:17:13 -08:00
emily e0b6ea9f8e Added devel webpack build job 2023-02-09 00:16:27 -08:00
emily b92f24739b Update About and Source to point at new landing page 2022-12-11 17:06:51 -08:00
emily 23ca3cbcef Syles loader to no take mess with content-paint 2022-12-11 16:01:38 -08:00
5 changed files with 220 additions and 98 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ version: 0.2
phases:
install:
runtime-versions:
nodejs: 14
nodejs: 16
commands:
- echo Installing dependencies...
- npm install
+2 -1
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",
+52 -23
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>
+136 -70
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);
});
+29 -3
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;