mirror of
https://github.com/gabehf/CostInCoffee.git
synced 2026-03-07 13:38:16 -08:00
Version 1 done
This commit is contained in:
parent
8d130b1e7b
commit
582a50ab19
3 changed files with 215 additions and 42 deletions
43
index.css
43
index.css
|
|
@ -8,6 +8,7 @@
|
||||||
--brown-light: #52391e;
|
--brown-light: #52391e;
|
||||||
--blue: #05213f;
|
--blue: #05213f;
|
||||||
--red: #EF3E36;
|
--red: #EF3E36;
|
||||||
|
--red-dark: #d73831;
|
||||||
--green: #08670a;
|
--green: #08670a;
|
||||||
--green-light: #217623;
|
--green-light: #217623;
|
||||||
--purple: #750973;
|
--purple: #750973;
|
||||||
|
|
@ -93,7 +94,7 @@ button:hover {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscriptions {
|
#subscriptions {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +110,7 @@ button:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-subs-text {
|
.no-subs-text {
|
||||||
|
color: var(--text-light);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -117,6 +119,10 @@ button:hover {
|
||||||
.sub-item-container:nth-child(odd) {
|
.sub-item-container:nth-child(odd) {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
.sub-item-container {
|
||||||
|
overflow-x:hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-item {
|
.sub-item {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
|
|
@ -126,6 +132,21 @@ button:hover {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.del-sub-button {
|
||||||
|
background-color: var(--red);
|
||||||
|
color: var(--main-bg);
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
width: 40px;
|
||||||
|
border-radius: 0px;
|
||||||
|
position: absolute;
|
||||||
|
right: -40px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
.del-sub-button:hover {
|
||||||
|
background-color: var(--red-dark);
|
||||||
|
}
|
||||||
|
|
||||||
#add-sub-form * {
|
#add-sub-form * {
|
||||||
margin: 5px 0px;
|
margin: 5px 0px;
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +179,9 @@ input:focus {
|
||||||
#subPrice {
|
#subPrice {
|
||||||
width: 55px;
|
width: 55px;
|
||||||
}
|
}
|
||||||
|
#subName {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-autocomplete {
|
.ui-autocomplete {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
@ -169,7 +193,7 @@ input:focus {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
.ui-autocomplete li {
|
.ui-autocomplete li {
|
||||||
padding: 10px 25px;
|
padding: 10px 0px 8px 25px;
|
||||||
}
|
}
|
||||||
.ui-autocomplete li:hover {
|
.ui-autocomplete li:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -179,4 +203,19 @@ input:focus {
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
border: 2px solid var(--red);
|
border: 2px solid var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-light);
|
||||||
|
margin-bottom: 25px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
font-style: italic;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
14
index.html
14
index.html
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,300;0,400;1,300&family=Orelega+One&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,300;0,400;1,300;1,400&family=Orelega+One&display=swap');
|
||||||
</style>
|
</style>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
@ -21,6 +21,11 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div class="preface">
|
||||||
|
So often you see in ads that their subscription is just <i>the price of a coffee a week</i>
|
||||||
|
or something similar. So just how many coffees a day are you really spending on all these
|
||||||
|
subscriptions?
|
||||||
|
</div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Cost in Coffee</h1>
|
<h1>Cost in Coffee</h1>
|
||||||
<h2>See the true cost of your subscriptions in terms of cups of coffee a day.</h2>
|
<h2>See the true cost of your subscriptions in terms of cups of coffee a day.</h2>
|
||||||
|
|
@ -31,7 +36,7 @@
|
||||||
<p>cups of coffee a day.</p>
|
<p>cups of coffee a day.</p>
|
||||||
<p>That's <span id="cost-display">$0.00</span> of coffee.</p>
|
<p>That's <span id="cost-display">$0.00</span> of coffee.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="subscriptions">
|
<div id="subscriptions">
|
||||||
<div id="subscription-header">
|
<div id="subscription-header">
|
||||||
<p>Your Subscriptions</p>
|
<p>Your Subscriptions</p>
|
||||||
<button type="button" id="addSubButton">Add</button>
|
<button type="button" id="addSubButton">Add</button>
|
||||||
|
|
@ -47,6 +52,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>©2023 Gabe Farrell</p>
|
||||||
|
<a href="https://github.com/gabehf/CostInCoffee">View the source on GitHub</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="main.js"></script>
|
<script src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
200
main.js
200
main.js
|
|
@ -1,4 +1,13 @@
|
||||||
|
/*
|
||||||
|
* TODO:
|
||||||
|
* - (maybe) include coffee price somewhere
|
||||||
|
* - (maybe) coffee price selector
|
||||||
|
*/
|
||||||
|
|
||||||
/********* CONSTANTS AND GLOBAL VARS *********/
|
/********* CONSTANTS AND GLOBAL VARS *********/
|
||||||
|
|
||||||
|
// List of subscription services I can think of, for the
|
||||||
|
// form to auto-fill
|
||||||
const SubList = [
|
const SubList = [
|
||||||
'LinkedIn Premium',
|
'LinkedIn Premium',
|
||||||
'Netflix',
|
'Netflix',
|
||||||
|
|
@ -6,31 +15,106 @@ const SubList = [
|
||||||
'Crunchyroll',
|
'Crunchyroll',
|
||||||
'Youtube Premium',
|
'Youtube Premium',
|
||||||
'Skillshare',
|
'Skillshare',
|
||||||
|
'Max',
|
||||||
|
'Spotify',
|
||||||
|
'Tidal',
|
||||||
|
'Paramount+',
|
||||||
|
'Peacock',
|
||||||
|
'Disney+',
|
||||||
|
'Amazon Prime',
|
||||||
|
'Audible',
|
||||||
|
'Scribd',
|
||||||
|
'Apple TV+',
|
||||||
|
'Curiosity Stream',
|
||||||
|
'Kindle Unlimited'
|
||||||
]
|
]
|
||||||
|
|
||||||
let subscriptions = []
|
// list of prices to be auto-suggested in the form
|
||||||
let numSubs = 0
|
|
||||||
const pricePattern = /^(\$)?(\d+(\.\d{1,2})?)$/
|
|
||||||
const avgDaysInMonth = 30.475
|
|
||||||
let costOfCoffee = 2.50
|
|
||||||
let costTotal = 0
|
|
||||||
let addFormOpen = false
|
|
||||||
|
|
||||||
const PriceList = [
|
const PriceList = [
|
||||||
'$5',
|
'$5',
|
||||||
'$10',
|
'$10',
|
||||||
'$12',
|
'$12',
|
||||||
'$15',
|
'$15',
|
||||||
'$20'
|
'$20',
|
||||||
|
'$40',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// array of current subscriptions
|
||||||
|
let subscriptions = []
|
||||||
|
// pretty much just for IDing the HTML in the sub list. dunno why its global tbh
|
||||||
|
let numSubs = 0
|
||||||
|
// matches $40.00, 40.00, 40, but not €40, 4.000, 4., 50c, etc.
|
||||||
|
const pricePattern = /^(\$)?(\d+(\.\d{1,2})?)$/
|
||||||
|
// these two are for calculating the cost in coffee
|
||||||
|
const avgDaysInMonth = 30.475
|
||||||
|
let costOfCoffee = 2.50
|
||||||
|
// total cost of all the user's subscriptions
|
||||||
|
let costTotal = 0
|
||||||
|
// for opening/closing form
|
||||||
|
let addFormOpen = false
|
||||||
|
|
||||||
/***************** FUNCTION DEFINITIONS **************/
|
/***************** FUNCTION DEFINITIONS **************/
|
||||||
|
|
||||||
|
// rebuilds the sub list after one gets removed
|
||||||
|
// if we dont do this, the IDs in the HTML that we use for removing
|
||||||
|
// items starts to drift from the actual JS array
|
||||||
|
function updateSubList() {
|
||||||
|
numSubs = 0
|
||||||
|
console.log('rebuilding sub list...')
|
||||||
|
for (let sub of subscriptions) {
|
||||||
|
// add subscription to page
|
||||||
|
$('#addSubFormContainer').after(`
|
||||||
|
<div class="sub-item-container" id="subscription-${numSubs}">
|
||||||
|
<div class="sub-item">
|
||||||
|
<p>${sub.name}</p>
|
||||||
|
<p>${formatPrice(sub.price)}</p>
|
||||||
|
</div>
|
||||||
|
</div>`)
|
||||||
|
numSubs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler for form
|
||||||
|
function addSubFormHandler() {
|
||||||
|
if (!validatePrice()) {
|
||||||
|
// disallow submit
|
||||||
|
console.log('nope')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let price = processSubValue()
|
||||||
|
|
||||||
|
addSubscription($('#subName').val(), price)
|
||||||
|
|
||||||
|
subscriptions.push({
|
||||||
|
name: $('#subName').val(),
|
||||||
|
price: price,
|
||||||
|
})
|
||||||
|
localStorage.setItem('subscriptions', JSON.stringify(subscriptions))
|
||||||
|
|
||||||
|
$('#subPrice').val('')
|
||||||
|
$('#subName').val('')
|
||||||
|
|
||||||
|
$('#addSubFormContainer').hide()
|
||||||
|
addFormOpen = !addFormOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// updates the coffee and price displays
|
||||||
|
function updateDisplay() {
|
||||||
|
// update display
|
||||||
|
let dailyCost = costTotal / avgDaysInMonth
|
||||||
|
$('#cost-display').text(`${formatPrice(dailyCost)}`)
|
||||||
|
$('#coffee-count').text(`${(dailyCost / costOfCoffee).toFixed(2)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles logic for adding a new subscription
|
||||||
|
// note: not just UI logic, also updating total costs etc.
|
||||||
function addSubscription(name, price) {
|
function addSubscription(name, price) {
|
||||||
|
|
||||||
// make sure no sub text is hidden
|
// make sure no sub text is hidden
|
||||||
$('.no-subs-text').hide()
|
$('.no-subs-text').hide()
|
||||||
// inc. sub count
|
|
||||||
numSubs++
|
// add subscription to page
|
||||||
$('#addSubFormContainer').after(`
|
$('#addSubFormContainer').after(`
|
||||||
<div class="sub-item-container" id="subscription-${numSubs}">
|
<div class="sub-item-container" id="subscription-${numSubs}">
|
||||||
<div class="sub-item">
|
<div class="sub-item">
|
||||||
|
|
@ -38,14 +122,36 @@ function addSubscription(name, price) {
|
||||||
<p>${formatPrice(price)}</p>
|
<p>${formatPrice(price)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>`)
|
</div>`)
|
||||||
costTotal += price
|
|
||||||
|
|
||||||
// update display
|
// inc. sub count
|
||||||
let dailyCost = costTotal / avgDaysInMonth
|
numSubs++
|
||||||
$('#cost-display').text(`${formatPrice(dailyCost)}`)
|
|
||||||
$('#coffee-count').text(`${(dailyCost / costOfCoffee).toFixed(2)}`)
|
costTotal += price
|
||||||
|
updateDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handles logic for removing a subscription
|
||||||
|
// note: not just UI logic, also updating total costs etc.
|
||||||
|
function removeSubscription(index) {
|
||||||
|
// get price from sub array
|
||||||
|
let price = subscriptions[index].price
|
||||||
|
// remove sub from array
|
||||||
|
subscriptions.splice(index, 1)
|
||||||
|
// if no subs are left, show no-sub text
|
||||||
|
if (subscriptions.length == 0) {
|
||||||
|
$('.no-subs-text').show()
|
||||||
|
}
|
||||||
|
// remove sub from localstorage
|
||||||
|
localStorage.setItem('subscriptions', JSON.stringify(subscriptions))
|
||||||
|
// remove sub from ui
|
||||||
|
$(`#subscription-${index}`).remove()
|
||||||
|
// subtract price from totalcost
|
||||||
|
costTotal -= price
|
||||||
|
// update display
|
||||||
|
updateDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
// takes sub cost from form and returns it as an actual number
|
||||||
function processSubValue() {
|
function processSubValue() {
|
||||||
let priceString = $('#subPrice').val()
|
let priceString = $('#subPrice').val()
|
||||||
if (priceString[0] == '$') {
|
if (priceString[0] == '$') {
|
||||||
|
|
@ -54,10 +160,12 @@ function processSubValue() {
|
||||||
return Number(priceString)
|
return Number(priceString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// takes number and returns it as $X.XX string
|
||||||
function formatPrice(num) {
|
function formatPrice(num) {
|
||||||
return '$' + (Math.round(num * 100) / 100).toFixed(2)
|
return '$' + (Math.round(num * 100) / 100).toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checks cost in form against regex, updates UI for error
|
||||||
function validatePrice() {
|
function validatePrice() {
|
||||||
let priceString = $('#subPrice').val()
|
let priceString = $('#subPrice').val()
|
||||||
if (!priceString.match(pricePattern)) {
|
if (!priceString.match(pricePattern)) {
|
||||||
|
|
@ -85,8 +193,12 @@ if (localStorage.getItem('subscriptions') != null) {
|
||||||
|
|
||||||
$('#addSubButton').click(() => {
|
$('#addSubButton').click(() => {
|
||||||
if (!addFormOpen) {
|
if (!addFormOpen) {
|
||||||
|
$('#addSubButton').css('background-color', 'var(--brown)')
|
||||||
|
$('#addSubButton').css('color', 'var(--main-bg)')
|
||||||
$('#addSubFormContainer').show()
|
$('#addSubFormContainer').show()
|
||||||
} else {
|
} else {
|
||||||
|
$('#addSubButton').css('background-color', '')
|
||||||
|
$('#addSubButton').css('color', '')
|
||||||
$('#addSubFormContainer').hide()
|
$('#addSubFormContainer').hide()
|
||||||
}
|
}
|
||||||
addFormOpen = !addFormOpen
|
addFormOpen = !addFormOpen
|
||||||
|
|
@ -95,13 +207,14 @@ $('#addSubButton').click(() => {
|
||||||
$('#subName').autocomplete({
|
$('#subName').autocomplete({
|
||||||
source: SubList,
|
source: SubList,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
minLength: 0,
|
autoFocus: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
$('#subPrice').autocomplete({
|
$('#subPrice').autocomplete({
|
||||||
source: PriceList,
|
source: PriceList,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
minLength: 0,
|
minLength: 0,
|
||||||
|
autoFocus: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
$('#subPrice').on('focus', () => {
|
$('#subPrice').on('focus', () => {
|
||||||
|
|
@ -109,25 +222,36 @@ $('#subPrice').on('focus', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
$('#confirmSubButton').click(() => {
|
$('#confirmSubButton').click(() => {
|
||||||
if (!validatePrice()) {
|
addSubFormHandler()
|
||||||
// disallow submit
|
})
|
||||||
console.log('nope')
|
|
||||||
return
|
$('#add-sub-form').keypress((e) => {
|
||||||
}
|
if (e.which == 13) {
|
||||||
|
addSubFormHandler()
|
||||||
let price = processSubValue()
|
return false
|
||||||
|
}
|
||||||
addSubscription($('#subName').val(), price)
|
})
|
||||||
|
|
||||||
subscriptions.push({
|
// these two are UI logic for the remove subscription button
|
||||||
name: $('#subName').val(),
|
$('#subscriptions').on('mouseenter', '.sub-item-container', function() {
|
||||||
price: price,
|
$(this).append(`<button id="${$(this).attr('id') + '-del'}" class="del-sub-button">X</button>`);
|
||||||
})
|
$(`#${$(this).attr('id') + '-del'}`).animate({
|
||||||
localStorage.setItem('subscriptions', JSON.stringify(subscriptions))
|
left: "-=40",
|
||||||
|
}, 200, function() {
|
||||||
$('#subPrice').val('')
|
})
|
||||||
$('#subName').val('')
|
})
|
||||||
|
$('#subscriptions').on('mouseleave', '.sub-item-container', function() {
|
||||||
$('#addSubFormContainer').hide()
|
$(`#${$(this).attr('id') + '-del'}`).animate({
|
||||||
addFormOpen = !addFormOpen
|
left: "+=40",
|
||||||
|
}, 200, function() {
|
||||||
|
$(`#${$(this).attr('id') + '-del'}`).remove()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#subscriptions').on('click', '.del-sub-button', function() {
|
||||||
|
// get index of sub to delete
|
||||||
|
let subIndex = $(this).attr('id').split('-')[1]
|
||||||
|
removeSubscription(subIndex)
|
||||||
|
$('.sub-item-container').remove()
|
||||||
|
updateSubList()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue