diff --git a/ui/eslint.config.js b/ui/eslint.config.js
index 238d2e4..2cf9713 100644
--- a/ui/eslint.config.js
+++ b/ui/eslint.config.js
@@ -1,38 +1,38 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import react from 'eslint-plugin-react'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
+import js from "@eslint/js";
+import globals from "globals";
+import react from "eslint-plugin-react";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
export default [
- { ignores: ['dist'] },
+ { ignores: ["dist"] },
{
- files: ['**/*.{js,jsx}'],
+ files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
- ecmaVersion: 'latest',
+ ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
- sourceType: 'module',
+ sourceType: "module",
},
},
- settings: { react: { version: '18.3' } },
+ settings: { react: { version: "18.3" } },
plugins: {
react,
- 'react-hooks': reactHooks,
- 'react-refresh': reactRefresh,
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
- ...react.configs['jsx-runtime'].rules,
+ ...react.configs["jsx-runtime"].rules,
...reactHooks.configs.recommended.rules,
- 'react/jsx-no-target-blank': 'off',
- 'react-refresh/only-export-components': [
- 'warn',
+ "react/jsx-no-target-blank": "off",
+ "react-refresh/only-export-components": [
+ "warn",
{ allowConstantExport: true },
],
},
},
-]
+];
diff --git a/ui/index.html b/ui/index.html
index fae0330..125935a 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -1,16 +1,14 @@
+
+
+
+
+ HaRail
+
-
-
-
-
- HaRail
-
-
-
-
-
-
-
+
+
+
+
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 2bcbe4c..b42c276 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -11,7 +11,10 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.0",
+ "@mui/x-date-pickers": "^7.24.0",
"axios": "^1.7.9",
+ "dayjs": "^1.11.13",
+ "prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
@@ -25,6 +28,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
+ "prettier": "3.4.2",
"vite": "^6.0.5"
}
},
@@ -1374,6 +1378,92 @@
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
"license": "MIT"
},
+ "node_modules/@mui/x-date-pickers": {
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.24.0.tgz",
+ "integrity": "sha512-oBM9Yp2H3tJ7qoHB4APQJYxZG4rz6JD4CwLzbzD9o3r+E1HGpGSLhwK3rDEz9VEjbOq8893Z2TGYLLWoyjeFXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0",
+ "@mui/x-internals": "7.24.0",
+ "@types/react-transition-group": "^4.4.11",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-internals": {
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.0.tgz",
+ "integrity": "sha512-lYa/XLltxNMY8YAFDopIHrXda2EAoqMCilyGMuPMz+WTG+b+StlUKqtj8cgFPQ/sa5dQ2fR7R3KJdjLREKUrlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -2340,6 +2430,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -4303,6 +4399,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
diff --git a/ui/package.json b/ui/package.json
index 663c83e..d432ee1 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -13,7 +13,10 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.0",
+ "@mui/x-date-pickers": "^7.24.0",
"axios": "^1.7.9",
+ "dayjs": "^1.11.13",
+ "prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
@@ -27,6 +30,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
+ "prettier": "3.4.2",
"vite": "^6.0.5"
},
"browserslist": {
diff --git a/ui/src/App.jsx b/ui/src/App.jsx
index 8c54ed2..5569aee 100644
--- a/ui/src/App.jsx
+++ b/ui/src/App.jsx
@@ -1,7 +1,10 @@
-import React, { useState, useEffect } from 'react';
-import { Container, Card } from '@mui/material';
-import axios from 'axios';
-import RouteFinder from './RouteFinder.jsx';
+import { useState, useEffect } from "react";
+import { Container, Card } from "@mui/material";
+import axios from "axios";
+import RouteFinder from "./RouteFinder.jsx";
+import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
+import "dayjs/locale/en-il";
const App = () => {
const [stations, setStations] = useState([]);
@@ -9,10 +12,10 @@ const App = () => {
useEffect(() => {
const fetchStations = async () => {
try {
- const response = await axios.get('/harail/stations');
+ const response = await axios.get("/harail/stations");
setStations(response.data);
} catch (error) {
- console.error('Error fetching stations:', error);
+ console.error("Error fetching stations:", error);
}
};
@@ -21,12 +24,14 @@ const App = () => {
return (
-
- HaRail
-
-
-
-
+
+
+ HaRail
+
+
+
+
+
);
};
diff --git a/ui/src/RouteFinder.jsx b/ui/src/RouteFinder.jsx
index fd5d39f..cfc7e7c 100644
--- a/ui/src/RouteFinder.jsx
+++ b/ui/src/RouteFinder.jsx
@@ -1,5 +1,6 @@
-import React, { useState } from 'react';
-import axios from 'axios';
+import { useState } from "react";
+import axios from "axios";
+import PropTypes from "prop-types";
import {
Card,
MenuItem,
@@ -9,33 +10,37 @@ import {
List,
ListItem,
ListItemText,
-} from '@mui/material';
+} from "@mui/material";
+import { DatePicker } from "@mui/x-date-pickers/DatePicker";
+import dayjs from "dayjs";
-const convertToIsoTime = (hhmmTime) => {
- const currentDate = new Date(); // Get the current date
- const timeParts = hhmmTime.split(':');
+const convertToIsoTime = (date, hhmmTime) => {
+ const timeParts = hhmmTime.split(":");
return new Date(
- currentDate.getFullYear(),
- currentDate.getMonth(),
- currentDate.getDate(),
- parseInt(timeParts[0]),
- parseInt(timeParts[1])
+ Date.UTC(
+ date.year(),
+ date.month(),
+ date.date(),
+ parseInt(timeParts[0]),
+ parseInt(timeParts[1])
+ )
).toISOString();
};
const convertToHHMM = (isoTime) => {
const date = new Date(isoTime);
- const hours = date.getUTCHours().toString().padStart(2, '0');
- const minutes = date.getUTCMinutes().toString().padStart(2, '0');
+ const hours = date.getUTCHours().toString().padStart(2, "0");
+ const minutes = date.getUTCMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
};
const RouteFinder = ({ stations }) => {
- const [source, setSource] = useState('');
- const [destination, setDestination] = useState('');
- const [startTime, setStartTime] = useState('');
- const [endTime, setEndTime] = useState('');
+ const [source, setSource] = useState("");
+ const [destination, setDestination] = useState("");
+ const [date, setDate] = useState(dayjs());
+ const [startTime, setStartTime] = useState("");
+ const [endTime, setEndTime] = useState("");
const [routes, setRoutes] = useState([]);
const sortStations = (stations) => {
@@ -53,44 +58,53 @@ const RouteFinder = ({ stations }) => {
const handleSearch = async () => {
try {
// Make API request to /harail/routes/find with selected parameters
- const response = await axios.get('/harail/routes/find', {
+ const response = await axios.get("/harail/routes/find", {
params: {
search: "Multi",
start_station: source,
- start_time: convertToIsoTime(startTime),
+ start_time: convertToIsoTime(date, startTime),
end_station: destination,
- end_time: convertToIsoTime(endTime),
+ end_time: convertToIsoTime(date, endTime),
},
});
// Assuming the response contains an array of routes
setRoutes(response.data);
} catch (error) {
- console.error('Error fetching routes:', error);
+ console.error("Error fetching routes:", error);
}
};
return (
Route Finder
-
setSource(e.target.value)}>
+ onChange={(e) => setSource(e.target.value)}
+ >
{sortStations(stations).map((station) => (
))}
-
setDestination(e.target.value)}>
+ onChange={(e) => setDestination(e.target.value)}
+ >
{sortStations(stations).map((station) => (
))}
+
setDate(date)}
+ />
{
Routes:
{routes.map((route) => (
-
+
{route.parts.map((part) => (
- station.id === part.start_station).name} ל` +
- `${stations.find((station) => station.id === part.end_station).name} ` +
- `(${convertToHHMM(part.start_time)} - ${convertToHHMM(part.end_time)})`
- } />
+ station.id === part.start_station).name} ל` +
+ `${stations.find((station) => station.id === part.end_station).name} ` +
+ `(${convertToHHMM(part.start_time)} - ${convertToHHMM(part.end_time)})`
+ }
+ />
))}
@@ -137,4 +153,8 @@ const RouteFinder = ({ stations }) => {
);
};
+RouteFinder.propTypes = {
+ stations: PropTypes.array,
+};
+
export default RouteFinder;
diff --git a/ui/src/main.jsx b/ui/src/main.jsx
index e375110..403c66d 100644
--- a/ui/src/main.jsx
+++ b/ui/src/main.jsx
@@ -1,15 +1,15 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import { ThemeProvider } from '@mui/material/styles';
-import CssBaseline from '@mui/material/CssBaseline';
-import App from './App.jsx'
-import theme from './theme.jsx';
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { ThemeProvider } from "@mui/material/styles";
+import CssBaseline from "@mui/material/CssBaseline";
+import App from "./App.jsx";
+import theme from "./theme.jsx";
-createRoot(document.getElementById('root')).render(
+createRoot(document.getElementById("root")).render(
,
-)
+);
diff --git a/ui/src/theme.jsx b/ui/src/theme.jsx
index 96d3ada..77de82d 100644
--- a/ui/src/theme.jsx
+++ b/ui/src/theme.jsx
@@ -1,136 +1,136 @@
-import { createTheme } from '@mui/material/styles';
+import { createTheme } from "@mui/material/styles";
const theme = createTheme({
- palette: {
- primary: {
- main: '#3f51b5', // Indigo color for primary
- },
- secondary: {
- main: '#f50057', // Pink color for secondary
- },
+ palette: {
+ primary: {
+ main: "#3f51b5", // Indigo color for primary
},
- typography: {
- fontFamily: 'Roboto, Arial, sans-serif',
- h1: {
- fontSize: '2.5rem',
- fontWeight: 300,
- },
- h2: {
- fontSize: '2rem',
- fontWeight: 400,
- },
- body1: {
- fontSize: '1rem',
- },
+ secondary: {
+ main: "#f50057", // Pink color for secondary
},
- components: {
- MuiContainer: {
- styleOverrides: {
- root: {
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- padding: '16px',
- },
- },
- },
- MuiButton: {
- styleOverrides: {
- root: {
- textTransform: 'none',
- margin: '8px',
- padding: '10px 20px',
- },
- },
- },
- MuiCard: {
- styleOverrides: {
- root: {
- padding: '16px',
- margin: '16px 0',
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- },
- },
- },
- MuiFormControl: {
- styleOverrides: {
- root: {
- margin: '8px',
- minWidth: '120px',
- },
- },
- },
- MuiInputLabel: {
- styleOverrides: {
- root: {
- color: '#3f51b5',
- },
- },
- },
- MuiMenuItem: {
- styleOverrides: {
- root: {
- padding: '10px 20px',
- },
- },
+ },
+ typography: {
+ fontFamily: "Roboto, Arial, sans-serif",
+ h1: {
+ fontSize: "2.5rem",
+ fontWeight: 300,
+ },
+ h2: {
+ fontSize: "2rem",
+ fontWeight: 400,
+ },
+ body1: {
+ fontSize: "1rem",
+ },
+ },
+ components: {
+ MuiContainer: {
+ styleOverrides: {
+ root: {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: "16px",
+ },
+ },
+ },
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ textTransform: "none",
+ margin: "8px",
+ padding: "10px 20px",
+ },
+ },
+ },
+ MuiCard: {
+ styleOverrides: {
+ root: {
+ padding: "16px",
+ margin: "16px 0",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ },
+ },
+ MuiFormControl: {
+ styleOverrides: {
+ root: {
+ margin: "8px",
+ minWidth: "120px",
},
- MuiSelect: {
- styleOverrides: {
- root: {
- minWidth: '120px',
- },
- },
+ },
+ },
+ MuiInputLabel: {
+ styleOverrides: {
+ root: {
+ color: "#3f51b5",
},
- MuiTextField: {
- styleOverrides: {
- root: {
- margin: '8px',
- width: '100%',
- },
- },
+ },
+ },
+ MuiMenuItem: {
+ styleOverrides: {
+ root: {
+ padding: "10px 20px",
},
- MuiInputBase: {
- styleOverrides: {
- input: {
- padding: '10px 12px',
- },
- },
+ },
+ },
+ MuiSelect: {
+ styleOverrides: {
+ root: {
+ minWidth: "120px",
},
- MuiOutlinedInput: {
- styleOverrides: {
- root: {
- '& $notchedOutline': {
- borderColor: '#3f51b5',
- },
- '&$focused $notchedOutline': {
- borderColor: '#f50057',
- },
- },
- notchedOutline: {},
- },
+ },
+ },
+ MuiTextField: {
+ styleOverrides: {
+ root: {
+ margin: "8px",
+ width: "100%",
},
- MuiFormLabel: {
- styleOverrides: {
- root: {
- '&$focused': {
- color: '#f50057',
- },
- },
- focused: {},
- },
+ },
+ },
+ MuiInputBase: {
+ styleOverrides: {
+ input: {
+ padding: "10px 12px",
},
- MuiTypography: {
- styleOverrides: {
- root: {
- margin: '8px',
- },
- },
+ },
+ },
+ MuiOutlinedInput: {
+ styleOverrides: {
+ root: {
+ "& $notchedOutline": {
+ borderColor: "#3f51b5",
+ },
+ "&$focused $notchedOutline": {
+ borderColor: "#f50057",
+ },
+ },
+ notchedOutline: {},
+ },
+ },
+ MuiFormLabel: {
+ styleOverrides: {
+ root: {
+ "&$focused": {
+ color: "#f50057",
+ },
+ },
+ focused: {},
+ },
+ },
+ MuiTypography: {
+ styleOverrides: {
+ root: {
+ margin: "8px",
},
+ },
},
+ },
});
export default theme;
diff --git a/ui/vite.config.js b/ui/vite.config.js
index c4355ac..22306eb 100644
--- a/ui/vite.config.js
+++ b/ui/vite.config.js
@@ -1,8 +1,8 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
- appType: 'mpa',
-})
+ appType: "mpa",
+});