// Setup subscriptions = [] currencies = new Map() currencies.set("$", 6.78) currencies.set("€", 7.47) currencies.set("DKK", 1) // The name of the data in localStorage localDataName = "subscriptionInformation" localSortPreferenceName = "sortPreference" localHasChangedOrder = "hasChangedOrder" // 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; } // 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. let isValidSubscription = (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. if (subscriptions.map((sub) => sub.title).includes(subscription.title)) { alert("Subscription name not unique") return false } return true } // Create a new subscription let newSubscription = () => { // Open the popup Swal.fire({ title: 'Insert subscription information', html: '
' + ' ' + '' + ' ' + '' + ' ' + '' + ' ' + '' + ' ' + '', focusConfirm: false, preConfirm: () => { // Create a new temporary subscription let newSub = { title: document.getElementById('swal-input1').value, amount: document.getElementById('swal-input2').value, currency: document.getElementById('swal-input3').value, months: document.getElementById('swal-input4').value, notes: document.getElementById('swal-input5').value, changed: false } // Do some form validation let valid = isValidSubscription(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 Swal.fire({ title: 'Insert subscription information', html: '
' + ' ' + '' + ' ' + '' + ' ' + '' + ' ' + '' + ' ' + '', focusConfirm: false, cancelButtonColor: '#d33', showCancelButton: true, preConfirm: () => { // Create a new temporary subscription let newSub = { title: document.getElementById('swal-input1').value, amount: document.getElementById('swal-input2').value, currency: document.getElementById('swal-input3').value, months: document.getElementById('swal-input4').value, notes: document.getElementById('swal-input5').value, changed: false } let valid = isValidSubscription(newSub) if (valid){ // Update the table if form was validated successfully deleteSubscription(currentSubscription) subscriptions.push(newSub) updateTable() } } }) } // Add a new currency. Called by the add currency button let addCurrency = () => { // Fire popup Swal.fire({ title: 'Insert currency information', html: '' + '', focusConfirm: false, preConfirm: () => { // Register the new possible currency let newCurrency = { symbol: document.getElementById('swal-input1').value, rate: 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 = () => { 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 = [] updateTable() 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 total = 0 // 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. document.querySelector('#total').innerHTML = total.toFixed(2) document.querySelector('#amountOfSubscriptions').innerHTML = subscriptions.length // 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] var 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 = "is-selected" elem.changed = false } // Create the row. tr.innerHTML = '' + elem.title + '' + '' + elem.monthly.toFixed(2) + '' + '' + elem.currency + '' + '' + elem.indkk.toFixed(2) + '' + '' + elem.percentOfWhole.toFixed(2) + '%' + '' + '' + '' + '' + '' + '' + '' + `` + `` + `` + ''; // 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 (subscription of subscriptions) { console.log(subscription) data.push({y: subscription.percentOfWhole, label: subscription.title}) } // Create the chart with CanvasJS. 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 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) Swal.fire( currentSub.title, `${currentSub.amount}${currentSub.currency} over ${currentSub.months} months, which equates to
${currentSub.monthly}${currentSub.currency} monthly, or about ${currentSub.indkk} DKK
` + // Display notes only if there are some. (currentSub.notes === undefined ? "" : ( ` Notes:
`)), '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] } // 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) => { 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)) Swal.fire( 'Deleted!', 'Your file has been deleted.', 'success' ) } }) } let deleteSubscription = (subscription) => { subscriptions = subscriptions.filter(s => s.title != subscription.title) updateTable() } // Make subscriptions and currency ready to be stored as JSON let getSaveInfo = () => { return {subscriptions: subscriptions, keys: [...currencies.keys()], values: [...currencies.values()]} } // Parse a save object, and use it as the current info. let loadSaveInfo = (loadedSave) => { subscriptions = loadedSave.subscriptions currencies.clear() for (let i = 0; i < loadedSave.keys.length; i++) { currencies.set(loadedSave.keys[i], loadedSave.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; loadedSave = JSON.parse(lines); loadSaveInfo(loadedSave) 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) } } // 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 { loadSaveInfo(JSON.parse(getStored(localDataName))) // Find out if we want to sort the array of the users data. if (!getStored(localHasChangedOrder)){ 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) }