diff --git a/src/App.jsx b/src/App.jsx index 8f9fc11..f4b1e3a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,66 @@ +import { useState } from 'react'; +import LocationIQ from './api/LocationIQ'; +import SearchForm from './components/SearchForm'; +import SearchResult from './components/SearchResult'; +import SearchError from './components/SearchError'; +import HistoryList from './components/HistoryList'; import './App.css'; +const API_KEY = import.meta.env.VITE_API_KEY; +const BASE_URL = import.meta.env.VITE_BASE_URL; + function App() { + const [results, setResults] = useState([]); + const [error, setError] = useState(""); + + const addResult = (result) => { + setResults(current => { + return [...current, result]; + }); + }; + + const clearError = () => { setError(""); }; + + const performSearchAsync = (location) => { + clearError(); + const api = new LocationIQ(API_KEY, BASE_URL); + + return api.getLatLonAsync(location) + .then(result => addResult(result)) + .catch(error => setError(error.message)); + }; + + // same method in async/await style + // const performSearchAsync = async (location) => { + // clearError(); + // const api = new LocationIQ(API_KEY); + // try { + // const result = await api.getLatLonAsync(location); + // addResult(result); + // } catch (error) { + // setError(error.message); + // } + // }; + + const locationSubmitted = (location) => { + performSearchAsync(location); + }; + + const result = results[results.length - 1]; - return null; + return ( +
+
+

Get Latitude and Longitude

+ +
+
+ + + +
+
+ ); } -export default App; +export default App; \ No newline at end of file diff --git a/src/api/LocationIQ.js b/src/api/LocationIQ.js new file mode 100644 index 0000000..50a47cb --- /dev/null +++ b/src/api/LocationIQ.js @@ -0,0 +1,51 @@ +import axios from 'axios'; + +const US_BASE_URL = "https://us1.locationiq.com/v1/"; +const SEARCH_URL = 'search.php'; + +class LocationIQ { + + constructor(apiKey, baseUrl) { + this.apiKey = apiKey; + this.baseUrl = baseUrl || US_BASE_URL; + } + + getLatLonAsync(location) { + return axios.get( + `${this.baseUrl}${SEARCH_URL}`, { params: + { + key: this.apiKey, + q: location, + format: 'json' + }}) + .then(response => { + const { lat, lon } = response.data[0]; + return { location, latitude: Number(lat), longitude: Number(lon) }; + }) + .catch(error => { + const message = error.response.data.error; + throw { message }; + }); + } + + // same method in async/await style + // async getLatLonAsync(location) { + // try { + // const response = await axios.get( + // `${this.baseUrl}${SEARCH_URL}`, { params: + // { + // key: this.apiKey, + // q: location, + // format: 'json' + // }}); + + // const { lat: latitude, lon: longitude } = response.data[0]; + // return { location, latitude, longitude }; + // } catch (error) { + // const message = error.response.data.error; + // throw { message }; + // } + // } +} + +export default LocationIQ; \ No newline at end of file diff --git a/src/components/History.css b/src/components/History.css new file mode 100644 index 0000000..39a91b8 --- /dev/null +++ b/src/components/History.css @@ -0,0 +1,9 @@ +.History { + display: block; + padding: 0; + margin: 0; +} + +.History span { + margin-right: 1em; +} diff --git a/src/components/History.jsx b/src/components/History.jsx new file mode 100644 index 0000000..44abb06 --- /dev/null +++ b/src/components/History.jsx @@ -0,0 +1,18 @@ +import { ResultType } from '../types'; +import './History.css'; + +const History = ({ entry }) => { + return ( +
  • +

    { entry.location }

    + Latitude: { entry.latitude } + Longitude: { entry.longitude } +
  • + ); +}; + +History.propTypes = { + entry: ResultType.isRequired, +}; + +export default History; \ No newline at end of file diff --git a/src/components/HistoryList.css b/src/components/HistoryList.css new file mode 100644 index 0000000..4808bec --- /dev/null +++ b/src/components/HistoryList.css @@ -0,0 +1,3 @@ +.HistoryList ul { + padding: 0; +} \ No newline at end of file diff --git a/src/components/HistoryList.jsx b/src/components/HistoryList.jsx new file mode 100644 index 0000000..0487255 --- /dev/null +++ b/src/components/HistoryList.jsx @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types'; +import History from './History'; +import { ResultType } from '../types'; +import './HistoryList.css'; + +const HistoryList = ({ entries }) => { + return ( +
    +

    Search History

    + +
    + ); +}; + +HistoryList.propTypes = { + entries: PropTypes.arrayOf(ResultType).isRequired, +}; + +export default HistoryList; \ No newline at end of file diff --git a/src/components/SearchError.css b/src/components/SearchError.css new file mode 100644 index 0000000..b376fe5 --- /dev/null +++ b/src/components/SearchError.css @@ -0,0 +1,7 @@ +.Error { + margin: 1rem 0; + padding: 1rem; + border: solid 3px red; + background-color: pink; + border-radius: .5rem; +} diff --git a/src/components/SearchError.jsx b/src/components/SearchError.jsx new file mode 100644 index 0000000..abf6717 --- /dev/null +++ b/src/components/SearchError.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import './SearchError.css'; + +const SearchError = ({ error }) => { + if (!error) { + return null; + } + + return ( +
    +

    Uh oh! Error!

    +

    { error }

    +
    + ); +}; + +SearchError.propTypes = { + error: PropTypes.string.isRequired, +}; + +export default SearchError; \ No newline at end of file diff --git a/src/components/SearchForm.css b/src/components/SearchForm.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/SearchForm.jsx b/src/components/SearchForm.jsx new file mode 100644 index 0000000..1d1d23f --- /dev/null +++ b/src/components/SearchForm.jsx @@ -0,0 +1,39 @@ +import { useState } from 'react'; +import PropTypes from 'prop-types'; +import './SearchForm.css'; + +const DEFAULT_STATE = { + location: '', +}; + +const SearchForm = ({ onLocationSubmit }) => { + const [formValues, setFormValues] = useState(DEFAULT_STATE); + + const textInput = (e) => { + const fieldName = e.target.name; + const value = e.target.value; + setFormValues(current => { + return { ...current, [fieldName]: value }; + }); + }; + + const formSubmitted = (event) => { + event.preventDefault(); + onLocationSubmit(formValues.location); + }; + + return ( +
    +
    + + +
    +
    + ); +}; + +SearchForm.propTypes = { + onLocationSubmit: PropTypes.func.isRequired, +}; + +export default SearchForm; \ No newline at end of file diff --git a/src/components/SearchResult.css b/src/components/SearchResult.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/SearchResult.jsx b/src/components/SearchResult.jsx new file mode 100644 index 0000000..6b100d3 --- /dev/null +++ b/src/components/SearchResult.jsx @@ -0,0 +1,20 @@ +import { ResultType } from '../types'; +import './SearchResult.css'; + +const SearchResult = ({ result }) => { + return ( +
    +

    Results for: { result?.location }

    + +
    + ); +}; + +SearchResult.propTypes = { + result: ResultType, +}; + +export default SearchResult; \ No newline at end of file diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 0000000..c0e67c2 --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,7 @@ +import { shape, string, number } from 'prop-types'; + +export const ResultType = shape({ + location: string.isRequired, + latitude: number.isRequired, + longitude: number.isRequired, +});