Started moving project to typescript, fixed a bug when editing subscriptions.

This commit is contained in:
Lucas Winther
2020-11-14 02:14:14 +01:00
parent 14917c8819
commit 162172fce1
9 changed files with 1110 additions and 520 deletions
+3 -3
View File
@@ -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/
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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>
-2
View File
@@ -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
View File
@@ -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
+1
View File
File diff suppressed because one or more lines are too long
+640
View File
@@ -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)
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2018",
"sourceMap": true
},
"exclude": [
"node_modules"
]
}