{"id":2281,"date":"2020-10-05T17:29:24","date_gmt":"2020-10-05T17:29:24","guid":{"rendered":"https:\/\/www.codeastar.com\/?p=2281"},"modified":"2022-12-03T19:21:16","modified_gmt":"2022-12-03T19:21:16","slug":"easy-cheap-flights-seeker-web-app-with-flask-and-react","status":"publish","type":"post","link":"https:\/\/www.codeastar.com\/easy-cheap-flights-seeker-web-app-with-flask-and-react\/","title":{"rendered":"Easy Cheap Flights Seeker Web App with Flask and React"},"content":{"rendered":"\n
We have 1, 2, 3, 4, 5 months without updating the CodeAStar web site. It seems 2020 is not a good year for everybody. What we can do are, stay strong, stay positive and keep learning. So let’s continue on what we mentioned last time — Easy Cheap Flights Seeker Web App. Did I mention “Easy” last time? No, actually. But once you have followed our steps to build the Cheap Flights Seeker Web App, you will probably agree with us. <\/p>\n\n\n\n
Before we start to build our Easy Cheap Flights Seeker Web App, let we have a big picture of the Web App and its related components.<\/p>\n\n\n\n So we have: <\/p>\n\n\n\n The first component we build is the backend server that finds cheap flights. The good news is, we don’t need to start everything from scratch. Do you remember our past exercise, the command line based, Cheap Flights Checker?<\/a> Yes, that Python program can do our flight searching logic. It acts as a proxy for us to interact with the Skyscanner API<\/a>. Then we need to add the airport seeker logic from our previous post<\/a> into the Python program. Everything seems fine? Not yet. It is still a Python standalone program, what we need is a backend server. No problem, it is an easy task for us. We can build a Flask<\/a> backend server, just likes the way we did for building a weather forecast server<\/a> in the past.<\/p>\n\n\n\n In our backend server source, other than the major flight info API and the newly added airport API, we would like to add two more Skyscanner API requests as well. They are countries and currencies APIs. So we can provide market (country) and currency options for user to select. And our code should look like:<\/p>\n\n\n (you can find the complete source link from the bottom of this post) <\/p>\n\n\n\n To start the backend server, first of all, let insert your Rapid’s Skyscanner API key into your server environment. It should be: <\/p>\n\n\n\n Once the API is set, we can start our backend server (in development environment). Please note that for the initial startup, it may need several minutes to load up all country\/airport\/currency information from Skyscanner side. All the information will be stored in our TinyDB json file, thus reducing data loading time for future server startup.<\/p>\n\n\n\n Our backend server is lock-and-load’ed. It is time for building our web application frontend. Similar to the time we build our weather forecast frontend<\/a>, we use React as our library for building user interfaces. But this time, we use Material-UI<\/a> to fasten and standardize our frontend development. In a nutshell, we use Material-UI to make our task easy.<\/p>\n\n\n\n First things first, let’s create our React project.<\/p>\n\n\n\n Now it is the time to bring Material-UI to our project.<\/p>\n\n\n\n Our base is ready, what’s next? Do you remember the web app mock up we mentioned in previous post? Yes, this one.<\/p>\n\n\n\n So we are going to have some search boxes for selecting currency, market and location. It would be better if those boxes can do auto-complete. Then we will need date packer for selecting the flight date, a checkbox for selecting direct flight and then a button to submit the request. All of the components should be placed under a grid. thus we can easily arrange the location of each componet. Okay, we think we have the blueprint in our mind, let’s do the coding. <\/p>\n\n\n\n From our react template folder, we start adding our Flask backend URL for our frontend to interact with. Find the .env<\/strong> or .env.development<\/strong> file under the template folder, then add:<\/p>\n\n\n\n “Localhost:5000” is our Flask backend address, you may change it to your designated ip or port.<\/p>\n\n\n\n We start our coding from App.js<\/strong> file. You will see App.js itself is a React component. And the React library is all about using components to build an application. So we add our wanted components from the above web app mockup to App.js. Firstly, let’s build a blank App component.<\/p>\n\n\n It is a simple blank container with nothing inside, yet. As we need to add several components to build our Flights Seeker Web App, let’ start with those easy ones — Button and Checkbox. <\/p>\n\n\n Besides the presentation components, Button and Checkbox, we also added “state” and “method” to our program to handle our logic.<\/p>\n\n\n\n When we want to make a more complex component with its own specified operations, we write the component to a separate file then import it back to our main program (App.js in our case). As we have mentioned before, we would like to make an Auto Complete search box components. It would be applied to Currency, Market and Location. Since Location is the most complicated one among the other two, let’s get started, with Location.<\/p>\n\n\n\n We create a component folder with a Location component file as “LocationInputField.js”. So we have following file structure now:<\/p>\n\n\n\n On “LocationInputField.js”, as it is a search box, we then import TextField component from Material-UI into it. And we have mentioned the Auto Complete function, so we import Autocomplete component as well. Please note that the Autocomplete component is not fully released as of the current date (October, 2020). But it still suits well for what we asked for. Let’s get the package from Material-UI:<\/p>\n\n\n\n On our code, we then import core and lab components from Material-UI:<\/p>\n\n\n In order to reduce the number of API calls, we should search the client’s browser records for related records first. In our “LocationInputField” case, we search the client’s browser local storage for the airport list and selected airport. If there are no such records, we go to call our API and store the records back to the client’s browser.<\/p>\n\n\n When a user selects an airport, we store it as onClickLocation<\/em>, a property of our LocationInputField component. From the above codes, we only do record retrieving and sorting tasks. The auto complete action is purely handled by Material-UI’s Autocomplete component itself. The things we need to do are providing a location list for Autocomplete component to complete (what we have just done) and render the Autocomplete component.<\/p>\n\n\n After selecting a location, it will trigger an onChange <\/em>event. We then put the selected location into a property of our LocationInputField component. Thus our main page, App.js, which imports LocationInputField component can take the location value and interact with other components. <\/p>\n\n\n We do similar handling on CurrencyInputField and MarketInputField components. Then on our App.js file, we set the location value into our page state. Among with values from other components, we gather them, save them to local storage, create a backdrop layer to avoid user making changes on UI, and send a API request to our backend server.<\/p>\n\n\n After that, we receive a JSON response from backend server containing cheap flight information or system message. We don’t handle the output on App.js directly. We pass it to FlightInfoGrid component.<\/p>\n\n\n\n<\/figure>\n\n\n\n
\n
Easy Cheap Flights Backend<\/h3>\n\n\n\n
\ndb = TinyDB('skyscanner.json')\nCountries = db.table('Countries')\nCurrencies = db.table('Currencies')\n\ndef getCountries(headers):\n country_list = []\n request_start_time = time.time()\n url = ENDPOINT_PREFIX+f"reference\/v1.0\/countries\/en-US"\n response = requests.request("GET", url, headers=headers)\n if response.status_code != 200: handleAPIException(response.text, "getCountries")\n ss_countries = json.loads(response.text)\n Countries.insert_multiple(ss_countries["Countries"])\n ss_countries = ss_countries["Countries"]\n for country in ss_countries:\n country_list.append(country['Name']) \n return country_list, request_start_time \n\ndef getCurrencies(headers):\n url = ENDPOINT_PREFIX+f"reference\/v1.0\/currencies"\n response = requests.request("GET", url, headers=headers)\n if response.status_code != 200: handleAPIException(response.text, "getCurrencies")\n currencies_json = json.loads(response.text)\n for element in currencies_json["Currencies"]: \n currency = {}\n currency["Code"] = element["Code"]\n currency["Symbol"] = element["Symbol"]\n Currencies.upsert(currency, Query().Code == currency["Code"])\n<\/pre><\/div>\n\n\n
>export SKYSCAN_RAPID_API_KEY=YOUR_KEY (for Linux\/MacOS)\nor\n>$Env:SKYSCAN_RAPID_API_KEY=\"YOUR_KEY\" (for Windows PowerShell)\nor\n>set SKYSCAN_RAPID_API_KEY=YOUR_KEY (for Windwos command shell)<\/code><\/pre>\n\n\n\n
> python .\\main.py\nGet country information from Skyscanner API...\nGot country information\nGet airport information from Skyscanner API...\n100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 234\/234 [06:29<00:00, 1.66s\/it]\nGot airport information\nGet currency information from Skyscanner API...\nGot currency information<\/code><\/pre>\n\n\n\n
Easy Cheap Flights Frontend<\/h3>\n\n\n\n
$npx create-react-app ez_cheap_flights_web<\/code><\/pre>\n\n\n\n
$npm install @material-ui\/core<\/code><\/pre>\n\n\n\n
<\/figure>\n\n\n\n
Coding with Material-UI<\/h3>\n\n\n\n
REACT_APP_EZ_SKYSCANNER_API=http:\/\/localhost:5000\/<\/code><\/pre>\n\n\n\n
\nimport React from 'react';\nimport Container from '@material-ui\/core';\nimport '.\/App.css';\n\nclass App extends React.Component {\n\n constructor(props){\n super(props);\n const apiUrl = process.env.REACT_APP_EZ_SKYSCANNER_API; \n };\n\n render() {\n return (\n <container maxwidth="md">\n <\/container>\n );\n }\/\/render\n}\/\/class\n\nexport default App; \n<\/pre><\/div>\n\n\n
\nimport {Box, Grid, Container, Typography, Checkbox, FormControlLabel} from '@material-ui\/core';\n constructor(props){\n super(props);\n this.state = {\n find_status: '',\n flight_info: null,\n disable_flag: false,\n direct_flight_flag: true,\n }\n this.handleClick = this.handleClick.bind(this);\n };\n \n handleClick=() => {};\n render() {\n return (\n <Container maxWidth="md">\n <Grid container spacing={2}>\n <Grid item xs={12}>\n <Box bgcolor="info.main" color="info.contrastText" p={1}>\n <Typography variant="h4" component="h1">\n EZ Cheap Flights Seeker\n <\/Typography>\n <\/Box>\n <\/Grid>\n <Grid item lg={6}>\n <FormControlLabel\n control={\n <Checkbox checked={this.state.direct_flight_flag} onChange={this.handleDirectFlight} name="checkedB" color="primary" \/> \n }\n label="Direct flight only" disabled={this.state.disable_flag}\n \/> \n <\/Grid> \n <Grid item xs={12}>\n <Button variant="contained" fullWidth={true} color="primary" onClick={this.handleClick} disabled={this.isFindNotReady()}>Find Flights!<\/Button>\n <\/Grid>\n <\/Grid>\n <\/Container>\n );\n }\/\/render\n<\/pre><\/div>\n\n\n
Auto Complete in Material-UI <\/h3>\n\n\n\n
\/ez_cheap_flights_web\n \/src \n \/components\n LocationInputField.js\n App.js\n App.css\n \/public\n index.html\n package.json\n .env <\/code><\/pre>\n\n\n\n
$npm install @material-ui\/lab<\/code><\/pre>\n\n\n\n
\nimport Box from '@material-ui\/core\/Box';\nimport TextField from '@material-ui\/core\/TextField';\nimport Autocomplete from '@material-ui\/lab\/Autocomplete';\nimport Alert from '@material-ui\/lab\/Alert';\nimport Snackbar from '@material-ui\/core\/Snackbar';\n\nvar place_list=[];\nclass LocationInputField extends React.Component {\n....\n}\n<\/pre><\/div>\n\n\n
\n componentDidMount() {\n var cachedSelectedPlace;\n if (this.props.type === locationFrom)\n {\n cachedSelectedPlace = localStorage.getItem("selected_from");\n this.setState({ form_text: "From" });\n this.setState({ form_id: "From" });\n }\n else if (this.props.type === locationTo) \n { \n cachedSelectedPlace = localStorage.getItem("selected_to");\n this.setState({ form_text: "To" });\n this.setState({ form_id: "To" });\n }\/\/if type\n\n var cachedPlaces = localStorage.getItem("places");\n if (cachedPlaces) {\n place_list = JSON.parse(cachedPlaces);\n this.setState({ places: place_list})\n this.setState({ error_status: false })\n } else {\n fetch(this.props.url+"\/api\/places")\n .then(res => res.json())\n .then((data) => {\n data.sort((a, b) => a.PlaceName.localeCompare(b.PlaceName));\n localStorage.setItem('places', JSON.stringify(data));\n })\n .catch((error)=>{\n console.log("Got error")\n console.log(error);\n this.setState({ error_status: true, })\n })\n }\/\/if cachedMarket\n\n if (cachedSelectedPlace) \n {\n let selectedPlace= place_list.find(place => place.Iata === cachedSelectedPlace);\n if (selectedPlace) \n {\n this.setState({ selected_place: selectedPlace }) \n this.props.onClickLocation(selectedPlace.Iata); \n }\n }\/\/if (cachedSelectedCurrency) \n }\/\/componentDidMount\n<\/pre><\/div>\n\n\n
\n render() {\n return (\n <box>\n <autocomplete id="{this.state.form_id}" options="{this.state.places}" getoptionlabel="{option" ==""> option.PlaceName+", "+option.CountryName + " ("+option.Iata+")"}\n value={this.state.selected_place}\n onChange={this.handleChange}\n disabled={this.props.disable_flag}\n renderInput={params => (\n <textfield {...params}="" label="{this.state.form_text}" variant="outlined" fullwidth="">\n )}\n \/>\n <snackbar open="{this.state.error_status}" autohideduration="{6000}" anchororigin="{{vertical:'top'," horizontal:'right'}}="">\n <alert severity="error" onclose="{()" ==""> {this.setState({ error_status: false, })}}>Network error found on Location API<\/alert>\n <\/snackbar>\n <\/textfield><\/autocomplete><\/box> \n );\n }\/\/render\n<\/pre><\/div>\n\n\n
Submit the Flight Search<\/h3>\n\n\n\n
\n handleChange=(event, value) => {\n if (value != null)\n {\n let selectedPlace = this.state.places.find(place => place.Iata === value.Iata);\n\n if (selectedPlace)\n {\n this.setState({ selected_place: selectedPlace })\n this.props.onClickLocation(value.Iata);\n } \n }\/\/if value != null \n }\/\/handleOnChange\n<\/pre><\/div>\n\n\n
\n handleLocationFrom = (fromLocation) => {\n this.setState({ selected_location_from: fromLocation});\n }\/\/handleLocationFrom\n\n handleClick=() => {\n localStorage.setItem('selected_market', this.state.selected_market);\n localStorage.setItem('selected_currency', this.state.selected_currency);\n localStorage.setItem('selected_from', this.state.selected_location_from);\n localStorage.setItem('selected_to', this.state.selected_location_to);\n \/\/show backdrop\n this.setState({ backdrop_flag: true});\n this.setState({disable_flag: true});\n this.setState({find_status: "Searching flight info..."});\n this.setState({flight_info: null});\n \n let query_data = {\n market:this.state.selected_market,\n currency: this.state.selected_currency,\n date_depart:this.state.selected_departing,\n date_return:this.state.selected_returning,\n place_from:this.state.selected_location_from,\n place_to:this.state.selected_location_to,\n directFlag:this.state.direct_flight_flag, \n day_range: this.state.day_range,\n };\n axios.post(this.state.url+"api\/findflight", query_data)\n .then(res => {\n let json_res = res.data;\n let flight_info_group = json_res.flight_info_group;\n let message = "";\n if (flight_info_group === null)\n {\n message = ConstantClass.CONSTANT_ERROR_TAG;\n }\n else\n {\n let flight_info_array = flight_info_group.map(flight_info => { return flight_info; })\n this.setState({ flight_info: flight_info_array}); \n }\/\/if === error\n this.handleClose(message);\n })\n }; \/\/ handleClick\n<\/pre><\/div>\n\n\n
Flight Info Grid<\/h3>\n\n\n\n