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 (
+
+
+
+ Regional Rail Fares
+
+
+
+ {/* First Dropdown */}
+
+
+
+
+
+
+
+
+
+ {/* Second Dropdown */}
+
+
+
+
+
+
+
+ {rideData && ridingTimeDropDown && showTimeHelperText()}
+
+
+
+
+ {/* Radio Buttons */}
+
+
+
+
+
+ {/* 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';