From 43cdb1a788d35e6bec6a7d7e07ecb0b3063152f9 Mon Sep 17 00:00:00 2001 From: Nammi Choi Date: Wed, 20 Aug 2025 15:00:44 -0400 Subject: [PATCH 1/2] initial commit with solutions --- septa-fare-calculator/package.json | 42 ++++ septa-fare-calculator/public/index.html | 43 ++++ septa-fare-calculator/public/manifest.json | 25 ++ septa-fare-calculator/public/robots.txt | 3 + septa-fare-calculator/public/septa.png | Bin 0 -> 9576 bytes septa-fare-calculator/src/App.css | 193 +++++++++++++++ septa-fare-calculator/src/App.js | 246 +++++++++++++++++++ septa-fare-calculator/src/App.test.js | 8 + septa-fare-calculator/src/index.css | 0 septa-fare-calculator/src/index.js | 17 ++ septa-fare-calculator/src/logo.svg | 1 + septa-fare-calculator/src/reportWebVitals.js | 13 + septa-fare-calculator/src/setupTests.js | 5 + 13 files changed, 596 insertions(+) create mode 100644 septa-fare-calculator/package.json create mode 100644 septa-fare-calculator/public/index.html create mode 100644 septa-fare-calculator/public/manifest.json create mode 100644 septa-fare-calculator/public/robots.txt create mode 100644 septa-fare-calculator/public/septa.png create mode 100644 septa-fare-calculator/src/App.css create mode 100644 septa-fare-calculator/src/App.js create mode 100644 septa-fare-calculator/src/App.test.js create mode 100644 septa-fare-calculator/src/index.css create mode 100644 septa-fare-calculator/src/index.js create mode 100644 septa-fare-calculator/src/logo.svg create mode 100644 septa-fare-calculator/src/reportWebVitals.js create mode 100644 septa-fare-calculator/src/setupTests.js diff --git a/septa-fare-calculator/package.json b/septa-fare-calculator/package.json new file mode 100644 index 000000000..b79127873 --- /dev/null +++ b/septa-fare-calculator/package.json @@ -0,0 +1,42 @@ +{ + "name": "septa-fare-challenge", + "version": "0.1.0", + "private": true, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.2.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.6.4", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^13.5.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/septa-fare-calculator/public/index.html b/septa-fare-calculator/public/index.html new file mode 100644 index 000000000..aa069f27c --- /dev/null +++ b/septa-fare-calculator/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/septa-fare-calculator/public/manifest.json b/septa-fare-calculator/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/septa-fare-calculator/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/septa-fare-calculator/public/robots.txt b/septa-fare-calculator/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/septa-fare-calculator/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/septa-fare-calculator/public/septa.png b/septa-fare-calculator/public/septa.png new file mode 100644 index 0000000000000000000000000000000000000000..599c0b9425a362a4af6f4dccb615bbb07431ad43 GIT binary patch literal 9576 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 literal 0 HcmV?d00001 diff --git a/septa-fare-calculator/src/App.css b/septa-fare-calculator/src/App.css new file mode 100644 index 000000000..c336ceffe --- /dev/null +++ b/septa-fare-calculator/src/App.css @@ -0,0 +1,193 @@ +body { + margin: 0; + font-family: 'Helvetica', 'Arial', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #363636; +} + +label, legend { + font-weight: bold; +} + +input[type="radio"] { + /* min 24px required for radio buttons for accesibility */ + transform: scale(1.5); +} + +.radio-label { + font-weight: normal; + padding-left: 4px; +} + +.outer-wrapper { + display: flex; + flex-direction: column; + min-height: 100vh; + border: 2px solid #a3a3a3; + overflow: hidden; + max-width: 380px; + margin: 0 auto; +} + +.header { + background: #5a5a5a; + display: flex; + align-items: center; + justify-content: center; + padding: 16px 0; +} + +.septa-logo { + width: 40px; + height: 30px; + margin-right: 10px; +} + +.header-text { + color: white; + margin: 0; + font-size: 24px; +} + +.main { + flex: 1; + display: flex; + flex-direction: column; +} + +.section { + display: flex; + flex-direction: column; + border-bottom: 1px solid #ccc; +} + +/* Remove native select arrow */ +.custom-select select { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + width: 100%; + height: 40px; + background-color: white; +} + +/* Container */ +.custom-select { + position: relative; + display: inline-block; + width: 100%; + padding-top: 1rem; +} + +/* Dark green solid triangle (down) */ +.custom-select::after { + content: ""; + position: absolute; + right: 12px; + top: 70%; + transform: translateY(-50%); + pointer-events: none; + + /* Make a solid DOWN arrow using CSS borders */ + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 8px solid ; + /* down-facing filled triangle */ + transition: transform 0.2s ease; +} + +/* Rotate triangle UP when focused/open */ +.custom-select.open::after { + border-top: none; + border-bottom: 8px solid #5a5a5a; + /* up-facing filled triangle */ +} + +.content-wrapper { + margin: 1.5rem; + text-align: center; + display: flex; + flex-direction: column; + align-items: flexStart; + justify-content: center; +} + +.text-left { + text-align: left; +} + +.no-margin-bottom { + margin-bottom: 0; +} + +.helper-text { + color: rgb(73, 73, 73); + font-size: 14px; + padding: 0 2rem; +} + +.radio-container { + display: flex; + justify-content: center; +} + +.radio-items-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + height: 80px; + width: 35%; +} + +.centered-items-container { + display: flex; + justify-content: center; +} + +.centered-items-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + height: 80px; + width: 50%; +} + +.radio-button { + margin: 4px 8px 0; +} + +.ticket-unit { + align-items: start; + padding-left: 2rem; +} + +.ticket-quantity { + width: 100px; + height: 36px; + text-align: center; +} + +footer { + color: white; + background-color: #5b5b5b; + text-align: center; +} + +footer p { + font-size: 20px; + margin-bottom: 0; +} + +.footer-text { + margin: 0; + font-size: 4rem; + padding: 1rem; +} \ No newline at end of file diff --git a/septa-fare-calculator/src/App.js b/septa-fare-calculator/src/App.js new file mode 100644 index 000000000..c669c554e --- /dev/null +++ b/septa-fare-calculator/src/App.js @@ -0,0 +1,246 @@ +import { useEffect, useState } from "react"; +import "./App.css"; + +function camelCaseWithSpaces(str) { + return str + .replace(/_/g, " ") // replace underscores with spaces + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +function App() { + const [zoneDropdown, setZoneDropdown] = useState(""); + const [ridingTimeDropDown, setRidingTimeDropDown] = useState(""); + const [purchaseOption, setPurchaseOption] = useState(""); + const [ticketQuantity, setTicketQuantity] = useState(""); + const [openDropdown, setOpenDropdown] = useState(null); // tracks what’s open + const [rideData, setRideData] = useState([]); + + //data fetch errors + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await fetch( + "https://raw.githubusercontent.com/thinkcompany/code-challenges/refs/heads/master/septa-fare-calculator/fares.json" + ); + const data = await res.json(); + setRideData(data); + } catch (error) { + console.log(error); + setError(error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const showTimeHelperText = () => { + switch (ridingTimeDropDown) { + case "anytime": + return "Valid anytime"; + case "weekday": + return "Valid Monday through Friday, 4:00 a.m. - 7:00 p.m. On trains arriving or departing 30th Street Station, Suburban and Jefferson StationRide and Transfer"; + case "evening_weekend": + return "Valid weekdays after 7:00 p.m.; all day Saturday, Sunday and major holidays. On trains arriving or departing 30th Street Station, Suburban and Jefferson Station"; + default: + return ""; + } + }; + + const findPrice = () => { + let filteredZone; + let filteredFareType; + + let singleTicketPrice; + let finalTicketPrice; + + if (rideData && zoneDropdown && ridingTimeDropDown) { + filteredZone = rideData.zones.filter( + (ride) => ride.zone === parseInt(zoneDropdown) + ); + + if (filteredZone && ridingTimeDropDown) { + filteredFareType = filteredZone[0].fares.filter( + (fare) => fare.type === ridingTimeDropDown + ); + } + } + + if (filteredFareType && purchaseOption) { + singleTicketPrice = filteredFareType?.filter( + (option) => option.purchase === purchaseOption + )[0].price; + } + + if (singleTicketPrice && ticketQuantity > 0) { + finalTicketPrice = singleTicketPrice * ticketQuantity; + } + + return finalTicketPrice; + }; + + if (loading) { + return

Loading...

; + } + + if (error) { + return

{error}

; + } + + return ( +
+
+ Septa logo +

Regional Rail Fares

+
+ +
+ {/* First Dropdown */} +
+
+ +
+ +
+
+
+ + {/* Second Dropdown */} +
+
+ +
+ +
+

+ {rideData && ridingTimeDropDown && showTimeHelperText()} +

+
+
+ + {/* Radio Buttons */} +
+
+ Where will you purchase the fare? +
+
+
+ +
+
+ + +
+
+
+ + {/* We could add helper text for purchase location in the future like below. Leaving it out since it's not in design. */} + {/*
    + {rideData?.info && + Object.entries(rideData.info).slice(3,5).map(([time, info], i) => { + return ( +
  • + {camelCaseWithSpaces(time)} : {info} +
  • + ); + })} +
*/} +
+
+ + {/* Input Field */} +
+
+ +
+
+ setTicketQuantity(e.target.value)} + placeholder="0" + className="ticket-quantity" + /> +
+
+
+
+
+ +
+

Your fare will cost:

+

+ ${findPrice() ? findPrice().toFixed(2) : "0.00"} +

+
+
+ ); +} + +export default App; diff --git a/septa-fare-calculator/src/App.test.js b/septa-fare-calculator/src/App.test.js new file mode 100644 index 000000000..1f03afeec --- /dev/null +++ b/septa-fare-calculator/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/septa-fare-calculator/src/index.css b/septa-fare-calculator/src/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/septa-fare-calculator/src/index.js b/septa-fare-calculator/src/index.js new file mode 100644 index 000000000..d563c0fb1 --- /dev/null +++ b/septa-fare-calculator/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/septa-fare-calculator/src/logo.svg b/septa-fare-calculator/src/logo.svg new file mode 100644 index 000000000..9dfc1c058 --- /dev/null +++ b/septa-fare-calculator/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/septa-fare-calculator/src/reportWebVitals.js b/septa-fare-calculator/src/reportWebVitals.js new file mode 100644 index 000000000..5253d3ad9 --- /dev/null +++ b/septa-fare-calculator/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/septa-fare-calculator/src/setupTests.js b/septa-fare-calculator/src/setupTests.js new file mode 100644 index 000000000..8f2609b7b --- /dev/null +++ b/septa-fare-calculator/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; From 6307217f9e0f55ebafc9d0b2a17fb8c818552674 Mon Sep 17 00:00:00 2001 From: Nammi Choi Date: Thu, 28 Aug 2025 14:48:50 -0400 Subject: [PATCH 2/2] loading status && input type number --- septa-fare-calculator/src/App.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/septa-fare-calculator/src/App.js b/septa-fare-calculator/src/App.js index c669c554e..2794afe68 100644 --- a/septa-fare-calculator/src/App.js +++ b/septa-fare-calculator/src/App.js @@ -22,6 +22,7 @@ function App() { const [loading, setLoading] = useState(false); useEffect(() => { + setLoading(true); const fetchData = async () => { try { const res = await fetch( @@ -221,7 +222,7 @@ function App() {
setTicketQuantity(e.target.value)} placeholder="0"