Started moving project to typescript, fixed a bug when editing subscriptions.
This commit is contained in:
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
# Include your project-specific ignores in this file
|
|
||||||
# Read about how to use .gitignore: https://help.github.com/articles/ignoring-files
|
/.idea/
|
||||||
# Useful .gitignore templates: https://github.com/github/gitignore
|
/node_modules/
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="TypeScriptCompiler">
|
||||||
|
<option name="recompileOnChanges" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -25,8 +25,6 @@
|
|||||||
your browser</a> to improve your experience and security.</p>
|
your browser</a> to improve your experience and security.</p>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
|
|
||||||
<button onclick="scrollToTopOrBottom()" id="myBtn" class="button">Bottom</button>
|
|
||||||
|
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-8 is-offset-2">
|
<div class="column is-8 is-offset-2">
|
||||||
<table class="table is-fullwidth is-bordered is-striped">
|
<table class="table is-fullwidth is-bordered is-striped">
|
||||||
|
|||||||
+216
-293
@@ -1,93 +1,79 @@
|
|||||||
// Setup
|
// Setup
|
||||||
subscriptions = []
|
let subscriptions = [];
|
||||||
|
const currencies = new Map();
|
||||||
currencies = new Map()
|
currencies.set("$", 6.78);
|
||||||
currencies.set("$", 6.78)
|
currencies.set("€", 7.47);
|
||||||
currencies.set("€", 7.47)
|
currencies.set("DKK", 1);
|
||||||
currencies.set("DKK", 1)
|
|
||||||
|
|
||||||
// The name of the data in localStorage
|
// The name of the data in localStorage
|
||||||
localDataName = "subscriptionInformation"
|
const localDataName = "subscriptionInformation";
|
||||||
localSortPreferenceName = "sortPreference"
|
let localSortPreferenceName = "sortPreference";
|
||||||
localHasChangedOrder = "hasChangedOrder"
|
|
||||||
|
|
||||||
// Initial scroll distance to bottom of the page.
|
// Initial scroll distance to bottom of the page.
|
||||||
let scrollDistance = getDocHeight()
|
let scrollDistance = getDocHeight();
|
||||||
|
|
||||||
// Function that top/bottom button calls. Scrolls to top if user has scrolled 50% down, bottom otherwise.
|
// Function that top/bottom button calls. Scrolls to top if user has scrolled 50% down, bottom otherwise.
|
||||||
let scrollToTopOrBottom = () => {
|
let scrollToTopOrBottom = () => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: scrollDistance,
|
top: scrollDistance,
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Gets the height of the page.
|
// Gets the height of the page.
|
||||||
function getDocHeight() {
|
function getDocHeight() {
|
||||||
let D = document;
|
let D = document;
|
||||||
return Math.max(
|
return Math.max(D.body.scrollHeight, D.documentElement.scrollHeight, D.body.offsetHeight, D.documentElement.offsetHeight, D.body.clientHeight, D.documentElement.clientHeight);
|
||||||
D.body.scrollHeight, D.documentElement.scrollHeight,
|
|
||||||
D.body.offsetHeight, D.documentElement.offsetHeight,
|
|
||||||
D.body.clientHeight, D.documentElement.clientHeight
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep track of the window height.
|
// Keep track of the window height.
|
||||||
let winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
let winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
||||||
window.onresize = () => {
|
window.onresize = () => {
|
||||||
winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Calculate whether the button should say top or bottom
|
|
||||||
window.onscroll = () => {
|
|
||||||
let docHeight = getDocHeight()
|
|
||||||
let trackLength = docHeight - winheight
|
|
||||||
|
|
||||||
let scrollTop = window.pageYOffset || (document.documentElement || document.body.parentNode || document.body).scrollTop
|
|
||||||
let pctScrolled = Math.floor(scrollTop/trackLength * 100) // gets percentage scrolled (ie: 80 or NaN if tracklength == 0)
|
|
||||||
|
|
||||||
if (pctScrolled > 50){
|
|
||||||
document.querySelector('#myBtn').innerHTML = "Top";
|
|
||||||
scrollDistance = 0
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
document.querySelector('#myBtn').innerHTML = "Bottom"
|
|
||||||
scrollDistance = trackLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if valid subscription, false if not.
|
// Returns true if valid subscription, false if not.
|
||||||
let isValidSubscription = (subscription) => {
|
let isValidNewSubscription = (subscription) => {
|
||||||
if (isNaN(subscription.amount)) {
|
|
||||||
alert("Amount is not a number")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!([...currencies.keys()].includes(subscription.currency))) {
|
|
||||||
alert("Currency is not a valid currency")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (isNaN(subscription.months)) {
|
|
||||||
alert("Months are not a number")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title of subscription must be unique.
|
// Title of subscription must be unique.
|
||||||
if (subscriptions.map((sub) => sub.title).includes(subscription.title)) {
|
if (subscriptions.map((sub) => sub.title).includes(subscription.title)) {
|
||||||
alert("Subscription name not unique")
|
alert("Subscription name not unique");
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
return isValidSubscription(subscription);
|
||||||
return true
|
};
|
||||||
|
let isValidSubscription = (subscription) => {
|
||||||
|
if (isNaN(subscription.price)) {
|
||||||
|
alert("Amount is not a number");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!([...currencies.keys()].includes(subscription.currency))) {
|
||||||
|
alert("Currency is not a valid currency");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isNaN(subscription.months)) {
|
||||||
|
alert("Months are not a number");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let createNewSubscription = (title, price, currency, months, notes) => {
|
||||||
|
const newSub = {
|
||||||
|
changed: false,
|
||||||
|
currency: currency,
|
||||||
|
indkk: 0,
|
||||||
|
monthly: 0,
|
||||||
|
months: months,
|
||||||
|
notes: notes,
|
||||||
|
percentOfWhole: null,
|
||||||
|
price: price,
|
||||||
|
title: title
|
||||||
|
};
|
||||||
|
newSub.monthly = newSub.price / newSub.months;
|
||||||
|
newSub.indkk = newSub.monthly * currencies.get(newSub.currency);
|
||||||
|
return newSub;
|
||||||
|
};
|
||||||
// Create a new subscription
|
// Create a new subscription
|
||||||
let newSubscription = () => {
|
let newSubscription = () => {
|
||||||
// Open the popup
|
// Open the popup
|
||||||
|
// @ts-ignore
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Insert subscription information',
|
title: 'Insert subscription information',
|
||||||
html:
|
html: '<hr class="is-black">' +
|
||||||
'<hr class="is-black">' +
|
|
||||||
'<label class="label has-text-left">Title</label> ' +
|
'<label class="label has-text-left">Title</label> ' +
|
||||||
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="">' +
|
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="">' +
|
||||||
'<label class="label has-text-left">Amount</label> ' +
|
'<label class="label has-text-left">Amount</label> ' +
|
||||||
@@ -101,41 +87,35 @@ let newSubscription = () => {
|
|||||||
focusConfirm: false,
|
focusConfirm: false,
|
||||||
preConfirm: () => {
|
preConfirm: () => {
|
||||||
// Create a new temporary subscription
|
// Create a new temporary subscription
|
||||||
let newSub = {
|
const title = document.getElementById('swal-input1').value;
|
||||||
title: document.getElementById('swal-input1').value,
|
const price = parseFloat(document.getElementById('swal-input2').value);
|
||||||
amount: document.getElementById('swal-input2').value,
|
const currency = document.getElementById('swal-input3').value;
|
||||||
currency: document.getElementById('swal-input3').value,
|
const months = parseInt(document.getElementById('swal-input4').value);
|
||||||
months: document.getElementById('swal-input4').value,
|
const notes = document.getElementById('swal-input5').value;
|
||||||
notes: document.getElementById('swal-input5').value,
|
const newSub = createNewSubscription(title, price, currency, months, notes);
|
||||||
changed: false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do some form validation
|
// Do some form validation
|
||||||
let valid = isValidSubscription(newSub)
|
let valid = isValidNewSubscription(newSub);
|
||||||
|
|
||||||
// Update the table if form was validated successfully
|
// Update the table if form was validated successfully
|
||||||
if (valid) {
|
if (valid) {
|
||||||
subscriptions.push(newSub)
|
subscriptions.push(newSub);
|
||||||
updateTable()
|
updateTable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Edit a subscription
|
// Edit a subscription
|
||||||
let editSubscription = (btn) => {
|
let editSubscription = (btn) => {
|
||||||
// Find out which subscription we're editing
|
// Find out which subscription we're editing
|
||||||
let currentSubscription = getSubscriptionFromButton(btn)
|
let currentSubscription = getSubscriptionFromButton(btn);
|
||||||
|
|
||||||
// Open the popup, and fill in the current subscription information in the form
|
// Open the popup, and fill in the current subscription information in the form
|
||||||
|
// @ts-ignore
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Insert subscription information',
|
title: 'Insert subscription information',
|
||||||
html:
|
html: '<hr class="is-black">' +
|
||||||
'<hr class="is-black">' +
|
|
||||||
'<label class="label has-text-left">Title</label> ' +
|
'<label class="label has-text-left">Title</label> ' +
|
||||||
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="' + currentSubscription.title + '">' +
|
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="' + currentSubscription.title + '">' +
|
||||||
'<label class="label has-text-left">Amount</label> ' +
|
'<label class="label has-text-left">Amount</label> ' +
|
||||||
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="' + currentSubscription.amount + '">' +
|
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="' + currentSubscription.price + '">' +
|
||||||
'<label class="label has-text-left">Currency</label> ' +
|
'<label class="label has-text-left">Currency</label> ' +
|
||||||
'<input id="swal-input3" class="swal2-input" placeholder="Original Currency" value="' + currentSubscription.currency + '">' +
|
'<input id="swal-input3" class="swal2-input" placeholder="Original Currency" value="' + currentSubscription.currency + '">' +
|
||||||
'<label class="label has-text-left">Over how many months</label> ' +
|
'<label class="label has-text-left">Over how many months</label> ' +
|
||||||
@@ -147,57 +127,53 @@ let editSubscription = (btn) => {
|
|||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
preConfirm: () => {
|
preConfirm: () => {
|
||||||
// Create a new temporary subscription
|
// Create a new temporary subscription
|
||||||
let newSub = {
|
const title = document.getElementById('swal-input1').value;
|
||||||
title: document.getElementById('swal-input1').value,
|
const price = parseFloat(document.getElementById('swal-input2').value);
|
||||||
amount: document.getElementById('swal-input2').value,
|
const currency = document.getElementById('swal-input3').value;
|
||||||
currency: document.getElementById('swal-input3').value,
|
const months = parseInt(document.getElementById('swal-input4').value);
|
||||||
months: document.getElementById('swal-input4').value,
|
const notes = document.getElementById('swal-input5').value;
|
||||||
notes: document.getElementById('swal-input5').value,
|
const editedSub = createNewSubscription(title, price, currency, months, notes);
|
||||||
changed: false
|
let valid = isValidSubscription(editedSub);
|
||||||
}
|
console.log(valid);
|
||||||
|
|
||||||
let valid = isValidSubscription(newSub)
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
console.log(editedSub);
|
||||||
// Update the table if form was validated successfully
|
// Update the table if form was validated successfully
|
||||||
deleteSubscription(currentSubscription)
|
deleteSubscription(currentSubscription);
|
||||||
subscriptions.push(newSub)
|
subscriptions.push(editedSub);
|
||||||
updateTable()
|
updateTable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Add a new currency. Called by the add currency button
|
// Add a new currency. Called by the add currency button
|
||||||
let addCurrency = () => {
|
let addCurrency = () => {
|
||||||
// Fire popup
|
// Fire popup
|
||||||
|
// @ts-ignore
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Insert currency information',
|
title: 'Insert currency information',
|
||||||
html:
|
html: '<input id="swal-input1" class="swal2-input" placeholder="Currency Symbol">' +
|
||||||
'<input id="swal-input1" class="swal2-input" placeholder="Currency Symbol">' +
|
|
||||||
'<input id="swal-input2" class="swal2-input" placeholder="DKK Rate">',
|
'<input id="swal-input2" class="swal2-input" placeholder="DKK Rate">',
|
||||||
focusConfirm: false,
|
focusConfirm: false,
|
||||||
preConfirm: () => {
|
preConfirm: () => {
|
||||||
// Register the new possible currency
|
// Register the new possible currency
|
||||||
let newCurrency = {
|
let newCurrency = {
|
||||||
symbol: document.getElementById('swal-input1').value,
|
symbol: document.getElementById('swal-input1').value,
|
||||||
rate: document.getElementById('swal-input2').value,
|
rate: parseFloat(document.getElementById('swal-input2').value),
|
||||||
}
|
};
|
||||||
|
|
||||||
// Check that the rate is a number
|
// Check that the rate is a number
|
||||||
if (isNaN(newCurrency.rate)) {
|
if (isNaN(newCurrency.rate)) {
|
||||||
alert("DKK exchange rate is now a number")
|
alert("DKK exchange rate is now a number");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update with the new currency, and refresh the table
|
// Update with the new currency, and refresh the table
|
||||||
currencies.set(newCurrency.symbol, newCurrency.rate)
|
currencies.set(newCurrency.symbol, newCurrency.rate);
|
||||||
updateTable()
|
updateTable();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Delete all subscriptions.
|
// Delete all subscriptions.
|
||||||
let Reset = () => {
|
let Reset = () => {
|
||||||
|
// @ts-ignore
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Are you sure?',
|
title: 'Are you sure?',
|
||||||
text: "You won't be able to revert this!",
|
text: "You won't be able to revert this!",
|
||||||
@@ -208,69 +184,46 @@ let Reset = () => {
|
|||||||
confirmButtonText: 'Yes, delete it!'
|
confirmButtonText: 'Yes, delete it!'
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
subscriptions = []
|
subscriptions.length = 0;
|
||||||
updateTable()
|
updateTable();
|
||||||
Swal.fire(
|
// @ts-ignore
|
||||||
'Reset!',
|
Swal.fire('Reset!', 'Your subscriptions have been reset.', 'success');
|
||||||
'Your subscriptions have been reset.',
|
|
||||||
'success'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// Method for updating the table with new information.
|
// Method for updating the table with new information.
|
||||||
let updateTable = () => {
|
let updateTable = () => {
|
||||||
// Create a new replacement tbody
|
// Create a new replacement tbody
|
||||||
let newTbody = document.createElement('tbody');
|
let newTbody = document.createElement('tbody');
|
||||||
newTbody.classList.add("tbody")
|
newTbody.classList.add("tbody");
|
||||||
|
|
||||||
// Sort by users preference
|
// Sort by users preference
|
||||||
|
|
||||||
|
|
||||||
// Add rows from data in memory.
|
// Add rows from data in memory.
|
||||||
populateTable(newTbody)
|
populateTable(newTbody);
|
||||||
|
|
||||||
// Replace the old tbody with the new cleared one
|
// Replace the old tbody with the new cleared one
|
||||||
let oldTbody = document.querySelector('.tbody')
|
let oldTbody = document.querySelector('.tbody');
|
||||||
oldTbody.parentNode.replaceChild(newTbody, oldTbody)
|
oldTbody.parentNode.replaceChild(newTbody, oldTbody);
|
||||||
|
|
||||||
// Save the new information locally.
|
// Save the new information locally.
|
||||||
setStored(localDataName, JSON.stringify(getSaveInfo()))
|
setStored(localDataName, JSON.stringify(getSaveInfo()));
|
||||||
}
|
};
|
||||||
|
|
||||||
// Populate a tbody with subscriptions from the subscriptions array.
|
// Populate a tbody with subscriptions from the subscriptions array.
|
||||||
let populateTable = (tbody) => {
|
let populateTable = (tbody) => {
|
||||||
// Store the total cost of all subscriptions
|
// Store the total cost of all subscriptions
|
||||||
total = 0
|
let total = subscriptions.map((s) => s.monthly).reduce((acc, sub) => acc + sub);
|
||||||
|
|
||||||
// calculate the monthly cost in dkk of each item, and add it to the running total
|
|
||||||
for (elem of subscriptions) {
|
|
||||||
elem.monthly = elem.amount / elem.months
|
|
||||||
elem.indkk = elem.monthly * currencies.get(elem.currency)
|
|
||||||
total += elem.indkk
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tablefoot with total monthly cost and amount of subscriptions.
|
// Update tablefoot with total monthly cost and amount of subscriptions.
|
||||||
document.querySelector('#total').innerHTML = total.toFixed(2)
|
document.querySelector('#total').innerHTML = total.toFixed(2);
|
||||||
document.querySelector('#amountOfSubscriptions').innerHTML = subscriptions.length
|
document.querySelector('#amountOfSubscriptions').innerHTML = subscriptions.length.toString();
|
||||||
|
|
||||||
// Begin to create table rows
|
// Begin to create table rows
|
||||||
for (let i = 0; i < subscriptions.length; i++) {
|
for (let i = 0; i < subscriptions.length; i++) {
|
||||||
// Select a subscription on index i, and create a new empty row
|
// Select a subscription on index i, and create a new empty row
|
||||||
let elem = subscriptions[i]
|
let elem = subscriptions[i];
|
||||||
var tr = document.createElement('tr');
|
let tr = document.createElement('tr');
|
||||||
|
|
||||||
// Calculate the subscriptions percentage-wise cost of the monthly total.
|
// Calculate the subscriptions percentage-wise cost of the monthly total.
|
||||||
elem.percentOfWhole = elem.indkk / total * 100;
|
elem.percentOfWhole = elem.indkk / total * 100;
|
||||||
|
|
||||||
// If this element has been changed (ie. created, moved), mark it for the user.
|
// If this element has been changed (ie. created, moved), mark it for the user.
|
||||||
if (elem.changed) {
|
if (elem.changed) {
|
||||||
tr.classList = "is-selected"
|
tr.classList.add("is-selected");
|
||||||
elem.changed = false
|
elem.changed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the row.
|
// Create the row.
|
||||||
tr.innerHTML = '<td>' + elem.title + '</td>' +
|
tr.innerHTML = '<td>' + elem.title + '</td>' +
|
||||||
'<td>' + elem.monthly.toFixed(2) + '</td>' +
|
'<td>' + elem.monthly.toFixed(2) + '</td>' +
|
||||||
@@ -279,24 +232,21 @@ let populateTable = (tbody) => {
|
|||||||
'<td>' + elem.percentOfWhole.toFixed(2) + '%</td>' +
|
'<td>' + elem.percentOfWhole.toFixed(2) + '%</td>' +
|
||||||
'<td>' + '<button class="button" onclick="getInfoOfSubscription(this)">Info</button>' + '</td>' +
|
'<td>' + '<button class="button" onclick="getInfoOfSubscription(this)">Info</button>' + '</td>' +
|
||||||
'<td>' + '<button class="button" onclick="askToDeleteRow(this)">Delete</button>' + '</td>' +
|
'<td>' + '<button class="button" onclick="askToDeleteRow(this)">Delete</button>' + '</td>' +
|
||||||
'<td>' + `<button class="button" onclick="moveRow(this, -1)">Up</button>` + `<button class="button" onclick="moveRow(this, +1)">Down</button>` + `<button class="button" onclick="editSubscription(this)">Edit</button>` + '</td>';
|
'<td>' + `<button class="button" onclick="editSubscription(this)">Edit</button>` + '</td>';
|
||||||
|
|
||||||
// Append the new row
|
// Append the new row
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Create a pie-chart with the cost of each subscription
|
// Create a pie-chart with the cost of each subscription
|
||||||
let PieChart = () => {
|
let PieChart = () => {
|
||||||
|
|
||||||
// Get data ready for CanvasJS.
|
// Get data ready for CanvasJS.
|
||||||
let data = []
|
let data = [];
|
||||||
for (subscription of subscriptions) {
|
for (let subscription of subscriptions) {
|
||||||
console.log(subscription)
|
console.log(subscription);
|
||||||
data.push({y: subscription.percentOfWhole, label: subscription.title})
|
data.push({ y: subscription.percentOfWhole, label: subscription.title });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the chart with CanvasJS.
|
// Create the chart with CanvasJS.
|
||||||
|
// @ts-ignore
|
||||||
let chart = new CanvasJS.Chart("chartContainer", {
|
let chart = new CanvasJS.Chart("chartContainer", {
|
||||||
animationEnabled: true,
|
animationEnabled: true,
|
||||||
title: {
|
title: {
|
||||||
@@ -310,48 +260,44 @@ let PieChart = () => {
|
|||||||
dataPoints: data
|
dataPoints: data
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lastly, render it.
|
// Lastly, render it.
|
||||||
chart.render()
|
chart.render();
|
||||||
}
|
};
|
||||||
|
|
||||||
// Ask the user what to sort by, and then sorts the array.
|
// Ask the user what to sort by, and then sorts the array.
|
||||||
let Sort = async () => {
|
let Sort = async () => {
|
||||||
|
|
||||||
// The input options for SWAL2
|
// The input options for SWAL2
|
||||||
const inputOptions = new Promise((resolve) => {
|
const inputOptions = new Promise((resolve) => {
|
||||||
resolve({
|
resolve({
|
||||||
'Lex': 'Lexi.',
|
'Lex': 'Lexi.',
|
||||||
'Price': 'Price'
|
'Price': 'Price'
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
// Fire the popup
|
// Fire the popup
|
||||||
|
// @ts-ignore
|
||||||
const { value: sortBy } = await Swal.fire({
|
const { value: sortBy } = await Swal.fire({
|
||||||
title: 'Sort by what',
|
title: 'Sort by what',
|
||||||
input: 'radio',
|
input: 'radio',
|
||||||
inputOptions: inputOptions,
|
inputOptions: inputOptions,
|
||||||
inputValidator: (value) => {
|
inputValidator: (value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return 'You need to choose something!'
|
return 'You need to choose something!';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Sort the array, and save the sorting preference
|
// Sort the array, and save the sorting preference
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
if (sortBy == "Lex") {
|
if (sortBy == "Lex") {
|
||||||
setStored(localSortPreferenceName, "Lex")
|
setStored(localSortPreferenceName, "Lex");
|
||||||
sortSubscriptionsByName()
|
sortSubscriptionsByName();
|
||||||
updateTable()
|
updateTable();
|
||||||
} else if (sortBy == "Price") {
|
}
|
||||||
setStored(localSortPreferenceName, "Price")
|
else if (sortBy == "Price") {
|
||||||
sortSubscriptionsByPrice()
|
setStored(localSortPreferenceName, "Price");
|
||||||
updateTable()
|
sortSubscriptionsByPrice();
|
||||||
|
updateTable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// different sorters for subscriptions array
|
// different sorters for subscriptions array
|
||||||
let sortSubscriptionsByPrice = () => {
|
let sortSubscriptionsByPrice = () => {
|
||||||
subscriptions.sort((a, b) => {
|
subscriptions.sort((a, b) => {
|
||||||
@@ -362,8 +308,8 @@ let sortSubscriptionsByPrice = () => {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
let sortSubscriptionsByName = () => {
|
let sortSubscriptionsByName = () => {
|
||||||
subscriptions.sort((a, b) => {
|
subscriptions.sort((a, b) => {
|
||||||
if (a.title > b.title) {
|
if (a.title > b.title) {
|
||||||
@@ -373,64 +319,30 @@ let sortSubscriptionsByName = () => {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// Gets more information on a subscription, and displays it in a popup
|
// Gets more information on a subscription, and displays it in a popup
|
||||||
let getInfoOfSubscription = (btn) => {
|
let getInfoOfSubscription = (btn) => {
|
||||||
let currentSub = getSubscriptionFromButton(btn)
|
let currentSub = getSubscriptionFromButton(btn);
|
||||||
Swal.fire(
|
// @ts-ignore
|
||||||
currentSub.title,
|
Swal.fire(currentSub.title, `${currentSub.price}${currentSub.currency} over ${currentSub.months} months, which equates to <br>
|
||||||
`${currentSub.amount}${currentSub.currency} over ${currentSub.months} months, which equates to <br>
|
|
||||||
${currentSub.monthly}${currentSub.currency} monthly, or about ${currentSub.indkk} DKK <br> ` +
|
${currentSub.monthly}${currentSub.currency} monthly, or about ${currentSub.indkk} DKK <br> ` +
|
||||||
// Display notes only if there are some.
|
// Display notes only if there are some.
|
||||||
(currentSub.notes === undefined ? "" : (
|
(currentSub.notes === undefined ? "" : (`
|
||||||
`
|
|
||||||
Notes: <br>
|
Notes: <br>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" disabled>${currentSub.notes}</textarea>
|
<textarea class="textarea" disabled>${currentSub.notes}</textarea>
|
||||||
</div>
|
</div>
|
||||||
`)),
|
`)), 'question');
|
||||||
'question'
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since title is unique, this finds the title of the row from which a button was pressed, and returns that subscription from the subscription array
|
// Since title is unique, this finds the title of the row from which a button was pressed, and returns that subscription from the subscription array
|
||||||
let getSubscriptionFromButton = (button) => {
|
let getSubscriptionFromButton = (button) => {
|
||||||
let tr = button.parentNode.parentNode
|
let tr = button.parentNode.parentNode;
|
||||||
let title = tr.firstChild.textContent
|
let title = tr.firstChild.textContent;
|
||||||
return subscriptions.filter(s => s.title == title)[0]
|
return subscriptions.filter(s => s.title == title)[0];
|
||||||
}
|
};
|
||||||
|
|
||||||
// Moves a row by some direction (+x, -x)
|
|
||||||
let moveRow = (btn, direction) => {
|
|
||||||
|
|
||||||
// Find the subscriptions current index
|
|
||||||
let currentSub = getSubscriptionFromButton(btn)
|
|
||||||
let indexOfCurrentRow = subscriptions.indexOf(currentSub)
|
|
||||||
|
|
||||||
// If trying to move a subscription out of bounds
|
|
||||||
if (indexOfCurrentRow + direction < 0 || indexOfCurrentRow + direction >= subscriptions.length) {
|
|
||||||
Swal.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Oops...',
|
|
||||||
text: 'Cant move this object ' + (direction == -1 ? "up" : "down"),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Move the subscription
|
|
||||||
subscriptions[indexOfCurrentRow] = subscriptions[indexOfCurrentRow + direction]
|
|
||||||
subscriptions[indexOfCurrentRow + direction] = currentSub
|
|
||||||
currentSub.changed = true
|
|
||||||
updateTable()
|
|
||||||
|
|
||||||
// Store that the user has a custom order
|
|
||||||
setStored(localHasChangedOrder, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let askToDeleteRow = (btn) => {
|
let askToDeleteRow = (btn) => {
|
||||||
|
// @ts-ignore
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Are you sure?',
|
title: 'Are you sure?',
|
||||||
text: "You won't be able to revert this!",
|
text: "You won't be able to revert this!",
|
||||||
@@ -441,36 +353,44 @@ let askToDeleteRow = (btn) => {
|
|||||||
confirmButtonText: 'Yes, delete it!'
|
confirmButtonText: 'Yes, delete it!'
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
deleteSubscription(getSubscriptionFromButton(btn))
|
deleteSubscription(getSubscriptionFromButton(btn));
|
||||||
Swal.fire(
|
// @ts-ignore
|
||||||
'Deleted!',
|
Swal.fire('Deleted!', 'Your file has been deleted.', 'success');
|
||||||
'Your file has been deleted.',
|
|
||||||
'success'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
let deleteSubscription = (subscription) => {
|
let deleteSubscription = (subscription) => {
|
||||||
subscriptions = subscriptions.filter(s => s.title != subscription.title)
|
const index = subscriptions.map(s => s.title).indexOf(subscription.title);
|
||||||
updateTable()
|
subscriptions.splice(index, 1);
|
||||||
}
|
updateTable();
|
||||||
|
};
|
||||||
// Make subscriptions and currency ready to be stored as JSON
|
// Make subscriptions and currency ready to be stored as JSON
|
||||||
let getSaveInfo = () => {
|
let getSaveInfo = () => {
|
||||||
return {subscriptions: subscriptions, keys: [...currencies.keys()], values: [...currencies.values()]}
|
return { version: 1, subscriptions: subscriptions, keys: [...currencies.keys()], values: [...currencies.values()] };
|
||||||
}
|
};
|
||||||
|
|
||||||
// Parse a save object, and use it as the current info.
|
// Parse a save object, and use it as the current info.
|
||||||
let loadSaveInfo = (loadedSave) => {
|
let loadSaveInfo = (loadedSave, oldLoadedSave) => {
|
||||||
subscriptions = loadedSave.subscriptions
|
let actualSaveToUse = loadedSave;
|
||||||
|
if (oldLoadedSave !== null) {
|
||||||
currencies.clear()
|
// Old data
|
||||||
for (let i = 0; i < loadedSave.keys.length; i++) {
|
console.log("loading legacy data...");
|
||||||
currencies.set(loadedSave.keys[i], loadedSave.values[i])
|
const oldData = oldLoadedSave;
|
||||||
|
let newData = { keys: [], subscriptions: [], values: [], version: 0 };
|
||||||
|
newData.keys = oldData.keys;
|
||||||
|
newData.version = 1;
|
||||||
|
newData.values = oldData.values;
|
||||||
|
for (let sub of oldLoadedSave.subscriptions) {
|
||||||
|
const newSub = createNewSubscription(sub.title, sub.amount, sub.currency, sub.months, sub.notes);
|
||||||
|
newData.subscriptions.push(newSub);
|
||||||
}
|
}
|
||||||
|
actualSaveToUse = newData;
|
||||||
}
|
}
|
||||||
|
subscriptions = actualSaveToUse.subscriptions;
|
||||||
|
currencies.clear();
|
||||||
|
for (let i = 0; i < actualSaveToUse.keys.length; i++) {
|
||||||
|
currencies.set(actualSaveToUse.keys[i], actualSaveToUse.values[i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
// Save a local JSON version of the current state.
|
// Save a local JSON version of the current state.
|
||||||
let saveToFile = () => {
|
let saveToFile = () => {
|
||||||
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(getSaveInfo()));
|
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(getSaveInfo()));
|
||||||
@@ -480,59 +400,60 @@ let saveToFile = () => {
|
|||||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||||
downloadAnchorNode.click();
|
downloadAnchorNode.click();
|
||||||
downloadAnchorNode.remove();
|
downloadAnchorNode.remove();
|
||||||
}
|
};
|
||||||
|
|
||||||
// Load a local JSON version of some state.
|
// Load a local JSON version of some state.
|
||||||
let load = () => {
|
let load = () => {
|
||||||
var input, file, fr;
|
var input, file, fr;
|
||||||
|
|
||||||
if (typeof window.FileReader !== 'function') {
|
if (typeof window.FileReader !== 'function') {
|
||||||
alert("The file API isn't supported on this browser yet.");
|
alert("The file API isn't supported on this browser yet.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
input = document.getElementById('fileinput');
|
input = document.getElementById('fileinput');
|
||||||
if (!input) {
|
if (!input) {
|
||||||
alert("Um, couldn't find the fileinput element.");
|
alert("Um, couldn't find the fileinput element.");
|
||||||
} else if (!input.files) {
|
}
|
||||||
|
else if (!input.files) {
|
||||||
alert("This browser doesn't seem to support the `files` property of file inputs.");
|
alert("This browser doesn't seem to support the `files` property of file inputs.");
|
||||||
} else if (!input.files[0]) {
|
}
|
||||||
|
else if (!input.files[0]) {
|
||||||
alert("Please select a file before clicking 'Load'");
|
alert("Please select a file before clicking 'Load'");
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
file = input.files[0];
|
file = input.files[0];
|
||||||
fr = new FileReader();
|
fr = new FileReader();
|
||||||
fr.onload = receivedText;
|
fr.onload = receivedText;
|
||||||
fr.readAsText(file);
|
fr.readAsText(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function receivedText(e) {
|
function receivedText(e) {
|
||||||
let lines = e.target.result;
|
let lines = e.target.result;
|
||||||
loadedSave = JSON.parse(lines);
|
let loadedSave = JSON.parse(lines);
|
||||||
loadSaveInfo(loadedSave)
|
if (loadedSave.version == null) {
|
||||||
|
loadSaveInfo(null, loadedSave);
|
||||||
updateTable()
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
loadSaveInfo(loadedSave, null);
|
||||||
}
|
}
|
||||||
|
updateTable();
|
||||||
|
}
|
||||||
|
};
|
||||||
// Retrieve the key value pair vname, vvalue
|
// Retrieve the key value pair vname, vvalue
|
||||||
function getStored(vname) {
|
function getStored(vname) {
|
||||||
if (typeof (Storage) !== "undefined") {
|
if (typeof (Storage) !== "undefined") {
|
||||||
return localStorage.getItem(vname)
|
return localStorage.getItem(vname);
|
||||||
} else {
|
}
|
||||||
return getCookie(vname)
|
else {
|
||||||
|
return getCookie(vname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the key value pair vname, vvalue
|
// Store the key value pair vname, vvalue
|
||||||
function setStored(vname, vvalue) {
|
function setStored(vname, vvalue) {
|
||||||
if (typeof (Storage) !== "undefined") {
|
if (typeof (Storage) !== "undefined") {
|
||||||
return localStorage.setItem(vname, vvalue)
|
return localStorage.setItem(vname, vvalue);
|
||||||
} else {
|
}
|
||||||
return setCookie(vname, vvalue)
|
else {
|
||||||
|
return setCookie(vname, vvalue, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Fallback if browser doesn't support HTML5 localstorage
|
// Fallback if browser doesn't support HTML5 localstorage
|
||||||
function setCookie(cname, cvalue, exdays) {
|
function setCookie(cname, cvalue, exdays) {
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
@@ -540,7 +461,6 @@ function setCookie(cname, cvalue, exdays) {
|
|||||||
var expires = "expires=" + d.toUTCString();
|
var expires = "expires=" + d.toUTCString();
|
||||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(cname) {
|
function getCookie(cname) {
|
||||||
var name = cname + "=";
|
var name = cname + "=";
|
||||||
var decodedCookie = decodeURIComponent(document.cookie);
|
var decodedCookie = decodeURIComponent(document.cookie);
|
||||||
@@ -556,29 +476,32 @@ function getCookie(cname) {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Try to load data from local storage. If this fails, there's probably not any data to retrieve.
|
// Try to load data from local storage. If this fails, there's probably not any data to retrieve.
|
||||||
try {
|
try {
|
||||||
loadSaveInfo(JSON.parse(getStored(localDataName)))
|
let data = JSON.parse(getStored(localDataName));
|
||||||
|
if (data.version == null) {
|
||||||
|
loadSaveInfo(null, data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
loadSaveInfo(data, null);
|
||||||
|
}
|
||||||
// Find out if we want to sort the array of the users data.
|
// Find out if we want to sort the array of the users data.
|
||||||
if (!getStored(localHasChangedOrder)){
|
let userSortPref = getStored(localSortPreferenceName);
|
||||||
let userSortPref = getStored(localSortPreferenceName)
|
|
||||||
if (userSortPref === undefined) {
|
if (userSortPref === undefined) {
|
||||||
sortSubscriptionsByName()
|
sortSubscriptionsByName();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (userSortPref == "Price") {
|
if (userSortPref == "Price") {
|
||||||
sortSubscriptionsByPrice()
|
sortSubscriptionsByPrice();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sortSubscriptionsByName()
|
sortSubscriptionsByName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateTable();
|
||||||
}
|
}
|
||||||
updateTable()
|
catch (e) {
|
||||||
} catch (e) {
|
console.log("Didn't find any locally stored data");
|
||||||
console.log("Didn't find any locally stored data")
|
console.log(e);
|
||||||
console.log(e)
|
|
||||||
}
|
}
|
||||||
|
//# sourceMappingURL=main.js.map
|
||||||
File diff suppressed because one or more lines are too long
+640
@@ -0,0 +1,640 @@
|
|||||||
|
interface subscription{
|
||||||
|
title: String,
|
||||||
|
price: number,
|
||||||
|
currency: String,
|
||||||
|
months: number,
|
||||||
|
notes: String,
|
||||||
|
monthly: number,
|
||||||
|
indkk: number,
|
||||||
|
percentOfWhole: number,
|
||||||
|
changed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface oldSubscription{
|
||||||
|
title: String,
|
||||||
|
amount: number,
|
||||||
|
currency: String,
|
||||||
|
months: number,
|
||||||
|
notes: String,
|
||||||
|
monthly: number,
|
||||||
|
indkk: number,
|
||||||
|
percentOfWhole: number,
|
||||||
|
changed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface oldSave{
|
||||||
|
subscriptions: oldSubscription[],
|
||||||
|
keys: string[], //Currencies symbols
|
||||||
|
values: number[] // Currencies values
|
||||||
|
}
|
||||||
|
|
||||||
|
interface save{
|
||||||
|
version: number,
|
||||||
|
subscriptions: subscription[],
|
||||||
|
keys: String[], //Currencies symbols
|
||||||
|
values: number[] // Currencies values
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
let subscriptions: subscription[] = [];
|
||||||
|
|
||||||
|
const currencies:Map<String, number> = new Map()
|
||||||
|
currencies.set("$", 6.78)
|
||||||
|
currencies.set("€", 7.47)
|
||||||
|
currencies.set("DKK", 1)
|
||||||
|
|
||||||
|
// The name of the data in localStorage
|
||||||
|
const localDataName = "subscriptionInformation"
|
||||||
|
let localSortPreferenceName = "sortPreference"
|
||||||
|
|
||||||
|
// Initial scroll distance to bottom of the page.
|
||||||
|
let scrollDistance = getDocHeight()
|
||||||
|
|
||||||
|
// Function that top/bottom button calls. Scrolls to top if user has scrolled 50% down, bottom otherwise.
|
||||||
|
let scrollToTopOrBottom = () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: scrollDistance,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the height of the page.
|
||||||
|
function getDocHeight() {
|
||||||
|
let D = document;
|
||||||
|
return Math.max(
|
||||||
|
D.body.scrollHeight, D.documentElement.scrollHeight,
|
||||||
|
D.body.offsetHeight, D.documentElement.offsetHeight,
|
||||||
|
D.body.clientHeight, D.documentElement.clientHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of the window height.
|
||||||
|
let winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
||||||
|
window.onresize = () => {
|
||||||
|
winheight = window.innerHeight || (document.documentElement || document.body).clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if valid subscription, false if not.
|
||||||
|
let isValidNewSubscription = (subscription): Boolean => {
|
||||||
|
|
||||||
|
// Title of subscription must be unique.
|
||||||
|
if (subscriptions.map((sub) => sub.title).includes(subscription.title)) {
|
||||||
|
alert("Subscription name not unique")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValidSubscription(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValidSubscription = (subscription: subscription): boolean => {
|
||||||
|
if (isNaN(subscription.price)) {
|
||||||
|
alert("Amount is not a number")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!([...currencies.keys()].includes(subscription.currency))) {
|
||||||
|
alert("Currency is not a valid currency")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isNaN(subscription.months)) {
|
||||||
|
alert("Months are not a number")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let createNewSubscription = (title, price, currency, months, notes): subscription => {
|
||||||
|
const newSub: subscription = {
|
||||||
|
changed: false,
|
||||||
|
currency: currency,
|
||||||
|
indkk: 0,
|
||||||
|
monthly: 0,
|
||||||
|
months: months,
|
||||||
|
notes: notes,
|
||||||
|
percentOfWhole: null,
|
||||||
|
price: price,
|
||||||
|
title: title
|
||||||
|
}
|
||||||
|
|
||||||
|
newSub.monthly = newSub.price / newSub.months;
|
||||||
|
newSub.indkk = newSub.monthly * currencies.get(newSub.currency);
|
||||||
|
|
||||||
|
return newSub;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new subscription
|
||||||
|
let newSubscription = () => {
|
||||||
|
// Open the popup
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Insert subscription information',
|
||||||
|
html:
|
||||||
|
'<hr class="is-black">' +
|
||||||
|
'<label class="label has-text-left">Title</label> ' +
|
||||||
|
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="">' +
|
||||||
|
'<label class="label has-text-left">Amount</label> ' +
|
||||||
|
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="">' +
|
||||||
|
'<label class="label has-text-left">Currency</label> ' +
|
||||||
|
'<input id="swal-input3" class="swal2-input" placeholder="Original Currency" value="">' +
|
||||||
|
'<label class="label has-text-left">Over how many months</label> ' +
|
||||||
|
'<input id="swal-input4" class="swal2-input" placeholder="Over how many months" value="">' +
|
||||||
|
'<label class="label has-text-left">Other notes</label> ' +
|
||||||
|
'<textarea id="swal-input5" class="textarea" placeholder="Notes" rows="4" cols="4">' + '</textarea>',
|
||||||
|
focusConfirm: false,
|
||||||
|
preConfirm: () => {
|
||||||
|
// Create a new temporary subscription
|
||||||
|
|
||||||
|
|
||||||
|
const title = (<HTMLInputElement>document.getElementById('swal-input1')).value;
|
||||||
|
const price = parseFloat((<HTMLInputElement>document.getElementById('swal-input2')).value)
|
||||||
|
const currency = (<HTMLInputElement>document.getElementById('swal-input3')).value
|
||||||
|
const months = parseInt((<HTMLInputElement>document.getElementById('swal-input4')).value)
|
||||||
|
const notes = (<HTMLInputElement>document.getElementById('swal-input5')).value
|
||||||
|
|
||||||
|
const newSub = createNewSubscription(title, price, currency, months, notes)
|
||||||
|
|
||||||
|
// Do some form validation
|
||||||
|
let valid = isValidNewSubscription(newSub)
|
||||||
|
|
||||||
|
// Update the table if form was validated successfully
|
||||||
|
if (valid){
|
||||||
|
subscriptions.push(newSub)
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit a subscription
|
||||||
|
let editSubscription = (btn) => {
|
||||||
|
// Find out which subscription we're editing
|
||||||
|
let currentSubscription = getSubscriptionFromButton(btn)
|
||||||
|
|
||||||
|
// Open the popup, and fill in the current subscription information in the form
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Insert subscription information',
|
||||||
|
html:
|
||||||
|
'<hr class="is-black">' +
|
||||||
|
'<label class="label has-text-left">Title</label> ' +
|
||||||
|
'<input id="swal-input1" class="swal2-input" placeholder="Subscription title" value="' + currentSubscription.title + '">' +
|
||||||
|
'<label class="label has-text-left">Amount</label> ' +
|
||||||
|
'<input id="swal-input2" class="swal2-input" placeholder="Original Amount" value="' + currentSubscription.price + '">' +
|
||||||
|
'<label class="label has-text-left">Currency</label> ' +
|
||||||
|
'<input id="swal-input3" class="swal2-input" placeholder="Original Currency" value="' + currentSubscription.currency + '">' +
|
||||||
|
'<label class="label has-text-left">Over how many months</label> ' +
|
||||||
|
'<input id="swal-input4" class="swal2-input" placeholder="Over how many months" value="' + currentSubscription.months + '">' +
|
||||||
|
'<label class="label has-text-left">Other notes</label> ' +
|
||||||
|
'<textarea id="swal-input5" class="textarea" placeholder="Notes" rows="4" cols="4">' + currentSubscription.notes + '</textarea>',
|
||||||
|
focusConfirm: false,
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
showCancelButton: true,
|
||||||
|
preConfirm: () => {
|
||||||
|
// Create a new temporary subscription
|
||||||
|
const title = (<HTMLInputElement>document.getElementById('swal-input1')).value;
|
||||||
|
const price = parseFloat((<HTMLInputElement>document.getElementById('swal-input2')).value)
|
||||||
|
const currency = (<HTMLInputElement>document.getElementById('swal-input3')).value
|
||||||
|
const months = parseInt((<HTMLInputElement>document.getElementById('swal-input4')).value)
|
||||||
|
const notes = (<HTMLInputElement>document.getElementById('swal-input5')).value
|
||||||
|
|
||||||
|
const editedSub = createNewSubscription(title, price, currency, months, notes)
|
||||||
|
|
||||||
|
let valid: boolean = isValidSubscription(editedSub)
|
||||||
|
console.log(valid)
|
||||||
|
|
||||||
|
if (valid){
|
||||||
|
console.log(editedSub)
|
||||||
|
// Update the table if form was validated successfully
|
||||||
|
deleteSubscription(currentSubscription)
|
||||||
|
subscriptions.push(editedSub)
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new currency. Called by the add currency button
|
||||||
|
let addCurrency = () => {
|
||||||
|
// Fire popup
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Insert currency information',
|
||||||
|
html:
|
||||||
|
'<input id="swal-input1" class="swal2-input" placeholder="Currency Symbol">' +
|
||||||
|
'<input id="swal-input2" class="swal2-input" placeholder="DKK Rate">',
|
||||||
|
focusConfirm: false,
|
||||||
|
preConfirm: () => {
|
||||||
|
// Register the new possible currency
|
||||||
|
let newCurrency = {
|
||||||
|
symbol: (<HTMLInputElement>document.getElementById('swal-input1')).value,
|
||||||
|
rate: parseFloat((<HTMLInputElement>document.getElementById('swal-input2')).value),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the rate is a number
|
||||||
|
if (isNaN(newCurrency.rate)) {
|
||||||
|
alert("DKK exchange rate is now a number")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with the new currency, and refresh the table
|
||||||
|
currencies.set(newCurrency.symbol, newCurrency.rate)
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all subscriptions.
|
||||||
|
let Reset = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, delete it!'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
subscriptions.length = 0;
|
||||||
|
updateTable()
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire(
|
||||||
|
'Reset!',
|
||||||
|
'Your subscriptions have been reset.',
|
||||||
|
'success'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Method for updating the table with new information.
|
||||||
|
let updateTable = () => {
|
||||||
|
// Create a new replacement tbody
|
||||||
|
let newTbody = document.createElement('tbody');
|
||||||
|
newTbody.classList.add("tbody")
|
||||||
|
|
||||||
|
// Sort by users preference
|
||||||
|
|
||||||
|
// Add rows from data in memory.
|
||||||
|
populateTable(newTbody)
|
||||||
|
|
||||||
|
// Replace the old tbody with the new cleared one
|
||||||
|
let oldTbody = document.querySelector('.tbody')
|
||||||
|
oldTbody.parentNode.replaceChild(newTbody, oldTbody)
|
||||||
|
|
||||||
|
// Save the new information locally.
|
||||||
|
setStored(localDataName, JSON.stringify(getSaveInfo()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate a tbody with subscriptions from the subscriptions array.
|
||||||
|
let populateTable = (tbody) => {
|
||||||
|
// Store the total cost of all subscriptions
|
||||||
|
let total = subscriptions.map((s) => s.monthly).reduce((acc, sub) => acc + sub)
|
||||||
|
|
||||||
|
// Update tablefoot with total monthly cost and amount of subscriptions.
|
||||||
|
document.querySelector('#total').innerHTML = total.toFixed(2);
|
||||||
|
document.querySelector('#amountOfSubscriptions').innerHTML = subscriptions.length.toString();
|
||||||
|
|
||||||
|
// Begin to create table rows
|
||||||
|
for (let i = 0; i < subscriptions.length; i++) {
|
||||||
|
// Select a subscription on index i, and create a new empty row
|
||||||
|
let elem = subscriptions[i]
|
||||||
|
let tr = document.createElement('tr');
|
||||||
|
|
||||||
|
// Calculate the subscriptions percentage-wise cost of the monthly total.
|
||||||
|
elem.percentOfWhole = elem.indkk / total * 100;
|
||||||
|
|
||||||
|
// If this element has been changed (ie. created, moved), mark it for the user.
|
||||||
|
if (elem.changed) {
|
||||||
|
tr.classList.add("is-selected");
|
||||||
|
elem.changed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the row.
|
||||||
|
tr.innerHTML = '<td>' + elem.title + '</td>' +
|
||||||
|
'<td>' + elem.monthly.toFixed(2) + '</td>' +
|
||||||
|
'<td>' + elem.currency + '</td>' +
|
||||||
|
'<td>' + elem.indkk.toFixed(2) + '</td>' +
|
||||||
|
'<td>' + elem.percentOfWhole.toFixed(2) + '%</td>' +
|
||||||
|
'<td>' + '<button class="button" onclick="getInfoOfSubscription(this)">Info</button>' + '</td>' +
|
||||||
|
'<td>' + '<button class="button" onclick="askToDeleteRow(this)">Delete</button>' + '</td>' +
|
||||||
|
'<td>' + `<button class="button" onclick="editSubscription(this)">Edit</button>` + '</td>';
|
||||||
|
|
||||||
|
// Append the new row
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a pie-chart with the cost of each subscription
|
||||||
|
let PieChart = () => {
|
||||||
|
// Get data ready for CanvasJS.
|
||||||
|
let data = []
|
||||||
|
for (let subscription of subscriptions) {
|
||||||
|
console.log(subscription)
|
||||||
|
data.push({y: subscription.percentOfWhole, label: subscription.title})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the chart with CanvasJS.
|
||||||
|
// @ts-ignore
|
||||||
|
let chart = new CanvasJS.Chart("chartContainer", {
|
||||||
|
animationEnabled: true,
|
||||||
|
title: {
|
||||||
|
text: "Monthly Expenses"
|
||||||
|
},
|
||||||
|
data: [{
|
||||||
|
type: "pie",
|
||||||
|
startAngle: 240,
|
||||||
|
yValueFormatString: "##0.00\"%\"",
|
||||||
|
indexLabel: "{label} {y}",
|
||||||
|
dataPoints: data
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lastly, render it.
|
||||||
|
chart.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the user what to sort by, and then sorts the array.
|
||||||
|
let Sort = async () => {
|
||||||
|
|
||||||
|
// The input options for SWAL2
|
||||||
|
const inputOptions = new Promise((resolve) => {
|
||||||
|
resolve({
|
||||||
|
'Lex': 'Lexi.',
|
||||||
|
'Price': 'Price'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fire the popup
|
||||||
|
// @ts-ignore
|
||||||
|
const {value: sortBy} = await Swal.fire({
|
||||||
|
title: 'Sort by what',
|
||||||
|
input: 'radio',
|
||||||
|
inputOptions: inputOptions,
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return 'You need to choose something!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort the array, and save the sorting preference
|
||||||
|
if (sortBy) {
|
||||||
|
if (sortBy == "Lex") {
|
||||||
|
setStored(localSortPreferenceName, "Lex")
|
||||||
|
sortSubscriptionsByName()
|
||||||
|
updateTable()
|
||||||
|
} else if (sortBy == "Price") {
|
||||||
|
setStored(localSortPreferenceName, "Price")
|
||||||
|
sortSubscriptionsByPrice()
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// different sorters for subscriptions array
|
||||||
|
let sortSubscriptionsByPrice = () => {
|
||||||
|
subscriptions.sort((a, b) => {
|
||||||
|
if (a.indkk > b.indkk) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.indkk > a.indkk) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let sortSubscriptionsByName = () => {
|
||||||
|
subscriptions.sort((a, b) => {
|
||||||
|
if (a.title > b.title) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (b.title > a.title) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Gets more information on a subscription, and displays it in a popup
|
||||||
|
let getInfoOfSubscription = (btn) => {
|
||||||
|
let currentSub = getSubscriptionFromButton(btn)
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire(
|
||||||
|
currentSub.title,
|
||||||
|
`${currentSub.price}${currentSub.currency} over ${currentSub.months} months, which equates to <br>
|
||||||
|
${currentSub.monthly}${currentSub.currency} monthly, or about ${currentSub.indkk} DKK <br> ` +
|
||||||
|
// Display notes only if there are some.
|
||||||
|
(currentSub.notes === undefined ? "" : (
|
||||||
|
`
|
||||||
|
Notes: <br>
|
||||||
|
<div class="control">
|
||||||
|
<textarea class="textarea" disabled>${currentSub.notes}</textarea>
|
||||||
|
</div>
|
||||||
|
`)),
|
||||||
|
'question'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since title is unique, this finds the title of the row from which a button was pressed, and returns that subscription from the subscription array
|
||||||
|
let getSubscriptionFromButton = (button) => {
|
||||||
|
let tr = button.parentNode.parentNode
|
||||||
|
let title = tr.firstChild.textContent
|
||||||
|
return subscriptions.filter(s => s.title == title)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
let askToDeleteRow = (btn) => {
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, delete it!'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.value) {
|
||||||
|
deleteSubscription(getSubscriptionFromButton(btn))
|
||||||
|
// @ts-ignore
|
||||||
|
Swal.fire(
|
||||||
|
'Deleted!',
|
||||||
|
'Your file has been deleted.',
|
||||||
|
'success'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleteSubscription = (subscription) => {
|
||||||
|
const index = subscriptions.map(s => s.title).indexOf(subscription.title);
|
||||||
|
subscriptions.splice(index, 1);
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make subscriptions and currency ready to be stored as JSON
|
||||||
|
let getSaveInfo = (): save => {
|
||||||
|
return {version: 1, subscriptions: subscriptions, keys: [...currencies.keys()], values: [...currencies.values()]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a save object, and use it as the current info.
|
||||||
|
let loadSaveInfo = (loadedSave: save, oldLoadedSave: oldSave) => {
|
||||||
|
|
||||||
|
let actualSaveToUse = loadedSave;
|
||||||
|
|
||||||
|
if (oldLoadedSave !== null){
|
||||||
|
// Old data
|
||||||
|
|
||||||
|
console.log("loading legacy data...")
|
||||||
|
|
||||||
|
const oldData: oldSave = oldLoadedSave;
|
||||||
|
|
||||||
|
let newData: save = {keys: [], subscriptions: [], values: [], version: 0};
|
||||||
|
newData.keys = oldData.keys;
|
||||||
|
newData.version = 1;
|
||||||
|
newData.values = oldData.values;
|
||||||
|
|
||||||
|
for (let sub of oldLoadedSave.subscriptions){
|
||||||
|
const newSub: subscription = createNewSubscription(sub.title, sub.amount, sub.currency, sub.months, sub.notes);
|
||||||
|
newData.subscriptions.push(newSub);
|
||||||
|
}
|
||||||
|
|
||||||
|
actualSaveToUse = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
subscriptions = actualSaveToUse.subscriptions
|
||||||
|
|
||||||
|
currencies.clear()
|
||||||
|
for (let i = 0; i < actualSaveToUse.keys.length; i++) {
|
||||||
|
currencies.set(actualSaveToUse.keys[i], actualSaveToUse.values[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save a local JSON version of the current state.
|
||||||
|
let saveToFile = () => {
|
||||||
|
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(getSaveInfo()));
|
||||||
|
var downloadAnchorNode = document.createElement('a');
|
||||||
|
downloadAnchorNode.setAttribute("href", dataStr);
|
||||||
|
downloadAnchorNode.setAttribute("download", "save" + ".json");
|
||||||
|
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||||
|
downloadAnchorNode.click();
|
||||||
|
downloadAnchorNode.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a local JSON version of some state.
|
||||||
|
let load = () => {
|
||||||
|
var input, file, fr;
|
||||||
|
|
||||||
|
if (typeof window.FileReader !== 'function') {
|
||||||
|
alert("The file API isn't supported on this browser yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input = document.getElementById('fileinput');
|
||||||
|
if (!input) {
|
||||||
|
alert("Um, couldn't find the fileinput element.");
|
||||||
|
} else if (!input.files) {
|
||||||
|
alert("This browser doesn't seem to support the `files` property of file inputs.");
|
||||||
|
} else if (!input.files[0]) {
|
||||||
|
alert("Please select a file before clicking 'Load'");
|
||||||
|
} else {
|
||||||
|
file = input.files[0];
|
||||||
|
fr = new FileReader();
|
||||||
|
fr.onload = receivedText;
|
||||||
|
fr.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function receivedText(e) {
|
||||||
|
let lines = e.target.result;
|
||||||
|
let loadedSave = JSON.parse(lines);
|
||||||
|
|
||||||
|
if (loadedSave.version == null){
|
||||||
|
loadSaveInfo(null, loadedSave)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
loadSaveInfo(loadedSave, null)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the key value pair vname, vvalue
|
||||||
|
function getStored(vname) {
|
||||||
|
if (typeof (Storage) !== "undefined") {
|
||||||
|
return localStorage.getItem(vname)
|
||||||
|
} else {
|
||||||
|
return getCookie(vname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the key value pair vname, vvalue
|
||||||
|
function setStored(vname, vvalue) {
|
||||||
|
if (typeof (Storage) !== "undefined") {
|
||||||
|
return localStorage.setItem(vname, vvalue)
|
||||||
|
} else {
|
||||||
|
return setCookie(vname, vvalue, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Fallback if browser doesn't support HTML5 localstorage
|
||||||
|
function setCookie(cname, cvalue, exdays) {
|
||||||
|
var d = new Date();
|
||||||
|
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||||
|
var expires = "expires=" + d.toUTCString();
|
||||||
|
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(cname) {
|
||||||
|
var name = cname + "=";
|
||||||
|
var decodedCookie = decodeURIComponent(document.cookie);
|
||||||
|
var ca = decodedCookie.split(';');
|
||||||
|
for (var i = 0; i < ca.length; i++) {
|
||||||
|
var c = ca[i];
|
||||||
|
while (c.charAt(0) == ' ') {
|
||||||
|
c = c.substring(1);
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) == 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Try to load data from local storage. If this fails, there's probably not any data to retrieve.
|
||||||
|
try {
|
||||||
|
let data = JSON.parse(getStored(localDataName));
|
||||||
|
|
||||||
|
if (data.version == null){
|
||||||
|
loadSaveInfo(null, data);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
loadSaveInfo(data, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find out if we want to sort the array of the users data.
|
||||||
|
let userSortPref = getStored(localSortPreferenceName)
|
||||||
|
if (userSortPref === undefined){
|
||||||
|
sortSubscriptionsByName()
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if (userSortPref == "Price"){
|
||||||
|
sortSubscriptionsByPrice()
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
sortSubscriptionsByName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateTable()
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Didn't find any locally stored data")
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2018",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user