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
+
+ { entries.map((entry, i) => (
+
+ )) }
+
+
+ );
+};
+
+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 }
+
+ - Latitude: { result?.latitude }
+ - Longitude: { result?.longitude }
+
+
+ );
+};
+
+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,
+});