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 000000000..599c0b942 Binary files /dev/null and b/septa-fare-calculator/public/septa.png differ 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..2794afe68 --- /dev/null +++ b/septa-fare-calculator/src/App.js @@ -0,0 +1,247 @@ +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(() => { + setLoading(true); + 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" + /> +
+
+
+
+
+ + +
+ ); +} + +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';