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) => ( {station.name} ))} - setDestination(e.target.value)}> + onChange={(e) => setDestination(e.target.value)} + > {sortStations(stations).map((station) => ( {station.name} ))} + 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", +});