diff --git a/septa-fare-calculator/favicon.ico b/septa-fare-calculator/favicon.ico new file mode 100644 index 000000000..3954048ef Binary files /dev/null and b/septa-fare-calculator/favicon.ico differ diff --git a/septa-fare-calculator/index.html b/septa-fare-calculator/index.html index ba1a762c9..40b4d0e3a 100644 --- a/septa-fare-calculator/index.html +++ b/septa-fare-calculator/index.html @@ -1,10 +1,109 @@ - - - SEPTA Regional Rail Fare Calculator - - - - - \ No newline at end of file + + + + + SEPTA Regional Rail Fare Calculator + + + + + +
+
+ The logo of SEPTA Regional Rail, two opposite arrows dividing a red and blue backgroundRegional Rail Fares +
+
+ + +
+
+ + + Valid Monday through Friday, 4:00 a.m. - 7:00 p.m. On trains arriving + or departing 30th Street Station, Suburban and Jefferson Station +
+
+ Where will you purchase the fare? +
+ + + + +
+
+
+ + +
+
+
+ Your fare will cost + $28.00 +
+
+ + +
+ + diff --git a/septa-fare-calculator/src/css/index.css b/septa-fare-calculator/src/css/index.css new file mode 100644 index 000000000..385b5decc --- /dev/null +++ b/septa-fare-calculator/src/css/index.css @@ -0,0 +1,125 @@ +body { + height: 100vh; + display: flex; +} + +.widget-body { + border: 2px solid lightgrey; + width: 100%; + margin: auto; +} + +.widget-body, +.widget-section, +.widget-section div { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; +} + +.widget-section { + border-bottom: 1px solid lightgrey; + width: 100%; + height: 20vh; +} + +.widget-section input[type="number"], +.widget-section select { + font-size: 1.25em; +} + +.kioskOrOnboardContainer-label { + font-size: 1.5em; + margin: 5px; + color: grey; +} + +.widget-section select, +.widget-section input[type="number"] { + background: none; + text-align: left; + border: 1px solid lightgrey; + border-radius: 10px; + width: 75%; + padding: 0.5em; + color: grey; +} + +.widget-section input[type="number"] { + text-align: center; + color: black; +} + +.widget-section > label, +.widget-section > span:first-child { + margin: 0.5rem; + font-size: 1.25em; + font-family: Verdana, Geneva, Tahoma, sans-serif; + color: grey; +} + +.widget-header { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + font-size: 1.5em; + background-color: grey; + color: white; + height: 2em; + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-weight: bold; +} + +.widget-header img { + margin-right: 20px; +} + +.when-riding-helper { + color: grey; + font-size: 0.8rem; + width: 80%; + text-align: center; +} + +.kioskOrOnboardContainer input { + display: none; +} + +.kioskOrOnboardContainer-label { + padding: 5px; + border: 1px solid lightgrey; + border-radius: 10px; + width: 100%; + text-align: center; +} + +.checked-label { + background: grey; + color: white; + border: 2px solid grey; +} + +input:hover, +label:hover { + cursor: pointer; +} + +#price-calc { + font-size: 1.25em; + background: gray; + color: white; +} + +.price-calc-output-number { + font-size: 3em; +} + +@media screen and (min-width: 600px) { + body { + width: 30%; + margin: auto; + } +} diff --git a/septa-fare-calculator/fares.json b/septa-fare-calculator/src/data/fares.json similarity index 100% rename from septa-fare-calculator/fares.json rename to septa-fare-calculator/src/data/fares.json diff --git a/septa-fare-calculator/src/html/component.html b/septa-fare-calculator/src/html/component.html new file mode 100644 index 000000000..e69de29bb diff --git a/septa-fare-calculator/src/js/FareCalculator.js b/septa-fare-calculator/src/js/FareCalculator.js new file mode 100644 index 000000000..32b3c91b5 --- /dev/null +++ b/septa-fare-calculator/src/js/FareCalculator.js @@ -0,0 +1,67 @@ +class FareCalculator { + constructor( + whereGoing, + whenRiding, + purchasedAtOnboard, + numberOfRides, + fareZoneInfo + ) { + this.whereGoing = parseInt(whereGoing.split(" ")[1]) || 4; + this.whenRiding = + !whenRiding === "anytime" || !whenRiding === "weekday" + ? "evening_weekend" + : whenRiding; + this.purchasedAtOnboard = purchasedAtOnboard + ? "onboard_purchase" + : "advance_purchase"; + this.numberOfRides = numberOfRides || 4; + this.fareZoneInfo = fareZoneInfo || []; + } + + setWhereGoing(str) { + this.whereGoing = parseInt(str.split(" ")[1]); + } + + setWhenRiding(str) { + const whenRiding = + str === "anytime" || str === "weekday" ? str : "evening_weekend"; + this.whenRiding = whenRiding; + } + + setPurchasedAtOnboard(boolean) { + const purchasedAtOnboard = boolean + ? "onboard_purchase" + : "advance_purchase"; + this.purchasedAtOnboard = purchasedAtOnboard; + } + + setNumberOfRides(num) { + this.numberOfRides = num; + } + + calculateFare() { + let price; + const fareZone = this.fareZoneInfo.find((z) => z.zone === this.whereGoing); + + //because everything except anytime and weekday share same pricing, this parses the user's preference to coincide with the json. + //however, we kept the values as user-friendly labels in the html for screen readers + const fareType = fareZone.fares.filter((fare) => { + return fare.type === this.whenRiding; + }); + + //we can end the function early if "anytime" is selected, because that's all the info we need to get the user's price - only one anytime price exists per zone + if (this.whenRiding === "anytime") { + price = fareType[0].price; + + return price; + } + + const farePurchase = fareType.find( + (fare) => fare.purchase === this.purchasedAtOnboard + ); + + price = farePurchase.price; + if (this.numberOfRides > 9) return price; + return price * this.numberOfRides; + } +} diff --git a/septa-fare-calculator/src/js/index.js b/septa-fare-calculator/src/js/index.js new file mode 100644 index 000000000..7d1521d4a --- /dev/null +++ b/septa-fare-calculator/src/js/index.js @@ -0,0 +1,159 @@ +//Could have broken things up into more classes, but for the scope of this assignment didn't seem necessary. + +let fareInfo = {}; +let fareZones = []; + +let fareDefaults = { + whereGoing: "Zone 4", + whenRiding: "weekday", + purchasedAtOnboard: false, + numberOfRides: 4, +}; + +//SETUP +const getFareData = async () => { + const response = await fetch("./src/data/fares.json"); + + if (response.ok) { + const data = response.json(); + return data; + } else { + throw new Error(`Error (Code: ${response.status}) ${response.statusText}`); + } +}; + +const setFareData = (fareData) => { + fareInfo = fareData.info; + fareZones = fareData.zones; +}; + +const { whereGoing, whenRiding, purchasedAtOnboard, numberOfRides } = + fareDefaults; + +let WidgetFareCalculator = new FareCalculator( + whereGoing, + whenRiding, + purchasedAtOnboard, + numberOfRides +); + +const onLoad = async () => { + const fareData = await getFareData().catch((error) => error); + + await setFareData(fareData); + + WidgetFareCalculator.fareZoneInfo = fareZones; +}; + +//EVENT HANDLERS +const handleWhereGoingSelectChange = (e) => { + e.preventDefault(); + + const whereGoingValue = e.target.value; + WidgetFareCalculator.setWhereGoing(whereGoingValue); + displayPrice(); +}; + +const handleWhenRidingSelectChange = (e) => { + //anytime tickets require at least ten rides and adv purchase, so if anytime is selected, it will force it to be on 10 and kiosk if it isn't so. + e.preventDefault(); + + const whenRidingValue = e.target.value; + if (numRidesInput.value < 10 && whenRidingValue == "anytime") { + numRidesInput.value = 10; + } + WidgetFareCalculator.setWhenRiding(whenRidingValue); + displayPrice(); + displayHelperText(false); +}; + +const handleKioskOrOnboardButtonsClick = (e) => { + const purchasedAtOnboardBoolean = e.target.value === "kiosk" ? false : true; + WidgetFareCalculator.setPurchasedAtOnboard(purchasedAtOnboardBoolean); + + displayPrice(); + displayHelperText(true); +}; + +const handleKioskOrOnboardButtonsLabelChange = (e) => { + const currentBtn = e.target.labels[0]; + const prevBtn = document.querySelector(".checked-label"); + + console.log(currentBtn); + + prevBtn.classList.remove("checked-label"); + currentBtn.classList.add("checked-label"); +}; + +const handleNumRidesInputChange = (e) => { + const numRidesInputValue = e.target.value; + + //because anytime tickets start at 10 rides and adv purchase, if it is above 10, it will default to anytime and kiosk for customer convenience + //also disables the input for onboarding purchase after 10, since those must be purchased in advance + if (numRidesInputValue >= 10) { + whenRidingSelect.value = "anytime"; + kioskOrOnboardButtons[0].checked = true; + kioskOrOnboardButtons[1].disabled = true; + } + + if (numRidesInputValue < 10) { + kioskOrOnboardButtons[1].disabled = false; + } + + WidgetFareCalculator.setNumberOfRides(numRidesInputValue); + displayPrice(); +}; + +//EVENT LISTENERS +const whereGoingSelect = document.getElementById("where-going-select"); +const whenRidingSelect = document.getElementById("when-riding-select"); +const kioskOrOnboardButtons = document.getElementsByClassName( + "kioskOrOnboardRadio" +); +const numRidesInput = document.getElementById("num-rides-input"); + +whereGoingSelect.addEventListener("change", (e) => { + handleWhereGoingSelectChange(e); +}); + +whenRidingSelect.addEventListener("change", (e) => { + handleWhenRidingSelectChange(e); +}); + +for (let i = 0; i < kioskOrOnboardButtons.length; i++) { + kioskOrOnboardButtons[i].addEventListener("click", (e) => { + handleKioskOrOnboardButtonsClick(e); + handleKioskOrOnboardButtonsLabelChange(e); + }); +} + +numRidesInput.addEventListener("change", (e) => { + handleNumRidesInputChange(e); +}); + +//UTILITY +const displayPrice = () => { + const price = WidgetFareCalculator.calculateFare(); + + const displayPriceSpan = document.getElementsByClassName( + "price-calc-output-number" + )[0]; + + displayPriceSpan.innerHTML = `$${price.toFixed(2)}`; +}; + +const displayHelperText = (purchasedOrWhenRidingBoolean) => { + const helperTextSpan = document.getElementById("when-riding-helper-text"); + + const isAskingWherePurchased = purchasedOrWhenRidingBoolean; + + if (isAskingWherePurchased) { + const helperText = fareInfo[WidgetFareCalculator.purchasedAtOnboard]; + + helperTextSpan.innerHTML = helperText; + } else { + const helperText = fareInfo[WidgetFareCalculator.whenRiding]; + + helperTextSpan.innerHTML = helperText; + } +};