diff --git a/.gitignore b/.gitignore index b7925b9b..b7c6eff9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ package-lock.json /build/* # Editor/IDE .idea -packages/api/etc/ \ No newline at end of file +packages/api/etc/ + +# api values +.env \ No newline at end of file diff --git a/README.md b/README.md index 51ce35b0..90fa38f5 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,13 @@ navigation panel): 1. Main page with weather for the current location. 2. List page (may also be a flyout or any other kind of menu), where user can see the weather for all cities in the World (all available cities) sorted by country and city title. -3. Page with details about chosen (in a list from previous point) city. +3. Page with details about chosen (in a list from previous point) city. 4. Info page where user can see some information about the service. 5. Feedback page with a form for feedback on your site: please, create the form in survey style with some simple questions. Implement form submitting mechanism and save it in localStorage. For now don't bother yourself saving form data in DB. 6. Implement Dark/Light mode for your app. There should be some sort of switcher that changes current view. - ### P.S. -There is a branch _ created for you in original repository. Your task is to fork from this repo and work +There is a branch \_ created for you in original repository. Your task is to fork from this repo and work in your branches there. At the end of the day, you should create PRs against your branches in original repo. diff --git a/package.json b/package.json index fc5db483..c11b4a81 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "connect-history-api-fallback": "1.6.0", "cross-env": "6.0.3", "css-loader": "3.2.0", + "dotenv-webpack": "^8.0.0", "file-loader": "4.2.0", "html-webpack-plugin": "3.2.0", "husky": "3.0.8", diff --git a/src/Api/weatherApi.js b/src/Api/weatherApi.js new file mode 100644 index 00000000..d676624e --- /dev/null +++ b/src/Api/weatherApi.js @@ -0,0 +1,20 @@ +import axios from 'axios'; + +const instance = axios.create({ + baseURL: 'https://pfa.foreca.com/api/v1/', + timeout: 3000, + headers: { Authorization: `Bearer ${process.env.TOKEN}` } +}); + +export const getCurrentWeather = async (longitude, latitude) => { + const result = await instance.get(`/current/${longitude},${latitude}`); + return result.data; +}; + +export const getLocationByCoords = async (longitude, latitude) => { + const result = await instance.get(`/location/${longitude},${latitude}`); + return { + city: result.data.name, + country: result.data.country + }; +}; diff --git a/src/Components/HomePage/HomePage.jsx b/src/Components/HomePage/HomePage.jsx deleted file mode 100644 index 9b2aa0f6..00000000 --- a/src/Components/HomePage/HomePage.jsx +++ /dev/null @@ -1,10 +0,0 @@ -function HomePage() { - - return ( -
-

Home

-
- ); -} - -export default HomePage; \ No newline at end of file diff --git a/src/Components/Loader/Loader.jsx b/src/Components/Loader/Loader.jsx new file mode 100644 index 00000000..cddffe99 --- /dev/null +++ b/src/Components/Loader/Loader.jsx @@ -0,0 +1,7 @@ +import styles from './loader.css'; + +function Loader() { + return
; +} + +export default Loader; diff --git a/src/Components/Loader/loader.css b/src/Components/Loader/loader.css new file mode 100644 index 00000000..e2bcbab8 --- /dev/null +++ b/src/Components/Loader/loader.css @@ -0,0 +1,9 @@ +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 60px; + height: 60px; + animation: spin 2s linear infinite; + transform: translate(150%, 150%); +} diff --git a/src/Components/NavigationPanel/NavigationPanel.jsx b/src/Components/NavigationPanel/NavigationPanel.jsx new file mode 100644 index 00000000..b7419b1f --- /dev/null +++ b/src/Components/NavigationPanel/NavigationPanel.jsx @@ -0,0 +1,15 @@ +import { Link } from 'react-router-dom'; + +function NavigationPanel() { + return ( +
+ +
+ ); +} + +export default NavigationPanel; diff --git a/src/Components/AboutPage/AboutPage.jsx b/src/Components/Pages/AboutPage/AboutPage.jsx similarity index 100% rename from src/Components/AboutPage/AboutPage.jsx rename to src/Components/Pages/AboutPage/AboutPage.jsx diff --git a/src/Components/ContactPage/ContactPage.jsx b/src/Components/Pages/ContactPage/ContactPage.jsx similarity index 99% rename from src/Components/ContactPage/ContactPage.jsx rename to src/Components/Pages/ContactPage/ContactPage.jsx index a2ecc7a9..1b85d05f 100644 --- a/src/Components/ContactPage/ContactPage.jsx +++ b/src/Components/Pages/ContactPage/ContactPage.jsx @@ -1,5 +1,4 @@ function ContactPage() { - return (

Contact

diff --git a/src/Components/DetailedForesastPage/DetailedForecastPage.jsx b/src/Components/Pages/DetailedForesastPage/DetailedForecastPage.jsx similarity index 99% rename from src/Components/DetailedForesastPage/DetailedForecastPage.jsx rename to src/Components/Pages/DetailedForesastPage/DetailedForecastPage.jsx index 48f3a0e0..f0235d3f 100644 --- a/src/Components/DetailedForesastPage/DetailedForecastPage.jsx +++ b/src/Components/Pages/DetailedForesastPage/DetailedForecastPage.jsx @@ -1,5 +1,4 @@ function DetailedForecastPage() { - return (

Detailed Page

diff --git a/src/Components/FindLocationPage/FindLocationPage.jsx b/src/Components/Pages/FindLocationPage/FindLocationPage.jsx similarity index 76% rename from src/Components/FindLocationPage/FindLocationPage.jsx rename to src/Components/Pages/FindLocationPage/FindLocationPage.jsx index 821f3eb2..fda403e5 100644 --- a/src/Components/FindLocationPage/FindLocationPage.jsx +++ b/src/Components/Pages/FindLocationPage/FindLocationPage.jsx @@ -6,4 +6,4 @@ function FindLocation() { ); } -export default FindLocation; \ No newline at end of file +export default FindLocation; diff --git a/src/Components/Pages/HomePage/HomePage.jsx b/src/Components/Pages/HomePage/HomePage.jsx new file mode 100644 index 00000000..b9dee2bd --- /dev/null +++ b/src/Components/Pages/HomePage/HomePage.jsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { fetchUserLocationWeather } from './homePageActions'; +import SafeWeatherImage from '../../SafeImage/SafeWeatherImage'; +import Loader from '../../Loader/Loader'; +import { capitalize, toReadableDate, toReadableTime } from '../../../helpers/formatHelper'; +import { getLocation } from '../../../Store/selectors/geoSelectors'; +import { getCurrentWeather, getLoading } from './homePageSelectors'; + +function HomePage() { + const dispatch = useDispatch(); + const location = useSelector(getLocation); + const currentWeather = useSelector(getCurrentWeather); + const loading = useSelector(getLoading); + + useEffect(() => { + dispatch(fetchUserLocationWeather()); + }, []); + + return ( + <> +

Home

+ {location && ( +

+ {location.city}, {location.country} +

+ )} + {loading && } + {currentWeather &&

{toReadableDate(currentWeather.current.time)}

} + {currentWeather &&

{toReadableTime(currentWeather.current.time)}

} + {currentWeather &&

Probability of precipitation: {currentWeather.current.precipProb}%

} + {currentWeather &&

Pressure (hPa) {currentWeather.current.pressure}%

} + {currentWeather &&

Wind speed (m/s) {currentWeather.current.windSpeed}

} + {currentWeather &&

{capitalize(currentWeather.current.symbolPhrase)}

} + {currentWeather && } + + ); +} + +export default HomePage; diff --git a/src/Components/Pages/HomePage/homePageActions.js b/src/Components/Pages/HomePage/homePageActions.js new file mode 100644 index 00000000..01d3e65d --- /dev/null +++ b/src/Components/Pages/HomePage/homePageActions.js @@ -0,0 +1,36 @@ +import { getCurrentWeather } from '../../../Api/weatherApi'; +import { fetchUserGeoData } from '../../../Store/actions/geoActions'; +import { getPosition } from '../../../Store/selectors/geoSelectors'; + +export const HOMEPAGE_ACTIONS = { + USER_POSITION_WEATHER_REQUESTED: 'USER_POSITION_WEATHER_REQUESTED', + USER_POSITION_WEATHER_RECEIVED: 'USER_POSITION_WEATHER_RECEIVED', + USER_POSITION_WEATHER_FAILED: 'USER_POSITION_WEATHER_FAILED' +}; + +const userPositionWeatherRequested = () => ({ + type: HOMEPAGE_ACTIONS.USER_POSITION_WEATHER_REQUESTED +}) + +const userPositionWeatherReceived = ({ currentWeather }) => ({ + type: HOMEPAGE_ACTIONS.USER_POSITION_WEATHER_RECEIVED, + payload: { currentWeather } +}); + +const userPositionWeatherFailed = ({ error }) => ({ + type: HOMEPAGE_ACTIONS.USER_POSITION_WEATHER_FAILED, + payload: error +}); + +export const fetchUserLocationWeather = () => async (dispatch, getState) => { + dispatch(userPositionWeatherRequested()); + + try { + await dispatch(fetchUserGeoData()); + const { longitude, latitude } = getPosition(getState()); + const currentWeather = await getCurrentWeather(longitude, latitude); + dispatch(userPositionWeatherReceived({ currentWeather })); + } catch (error) { + dispatch(userPositionWeatherFailed(error)); + } +}; diff --git a/src/Components/Pages/HomePage/homePageInitialState.js b/src/Components/Pages/HomePage/homePageInitialState.js new file mode 100644 index 00000000..a4dc31ad --- /dev/null +++ b/src/Components/Pages/HomePage/homePageInitialState.js @@ -0,0 +1,7 @@ +const initialState = { + currentWeather: null, + loading: false, + error: null +}; + +export default initialState; diff --git a/src/Components/Pages/HomePage/homePageReducer.js b/src/Components/Pages/HomePage/homePageReducer.js new file mode 100644 index 00000000..de543afd --- /dev/null +++ b/src/Components/Pages/HomePage/homePageReducer.js @@ -0,0 +1,29 @@ +import initialState from './homePageInitialState'; +import { HOMEPAGE_ACTIONS } from './homePageActions'; + +export const homePageReducer = (state = initialState, { type, payload }) => { + switch (type) { + case HOMEPAGE_ACTIONS.USER_POSITION_WEATHER_REQUESTED: + return { + ...state, + loading: true, + error: null + }; + case HOMEPAGE_ACTIONS.USER_POSITION_WEATHER_RECEIVED: + return { + ...state, + loading: false, + currentWeather: payload.currentWeather + }; + case HOMEPAGE_ACTIONS.USER_POSITION_WEATHER_FAILED: + return { + ...state, + loading: false, + error: payload + }; + default: + return state; + } +}; + +export default homePageReducer; \ No newline at end of file diff --git a/src/Components/Pages/HomePage/homePageSelectors.js b/src/Components/Pages/HomePage/homePageSelectors.js new file mode 100644 index 00000000..f1bc3b62 --- /dev/null +++ b/src/Components/Pages/HomePage/homePageSelectors.js @@ -0,0 +1,2 @@ +export const getCurrentWeather = state => state.homePage.currentWeather; +export const getLoading = state => state.homePage.loading; \ No newline at end of file diff --git a/src/Components/SafeImage/SafeWeatherImage.jsx b/src/Components/SafeImage/SafeWeatherImage.jsx new file mode 100644 index 00000000..0d20e27a --- /dev/null +++ b/src/Components/SafeImage/SafeWeatherImage.jsx @@ -0,0 +1,18 @@ +import { useState } from 'react'; + +function SafeWeatherImage({ symbolCode }) { + const [isImageExist, setIsImageExist] = useState(true); + + return ( + isImageExist && ( + { + setIsImageExist(false); + }} + /> + ) + ); +} + +export default SafeWeatherImage; diff --git a/src/Store/actions/geoActions.js b/src/Store/actions/geoActions.js new file mode 100644 index 00000000..f82bc8cd --- /dev/null +++ b/src/Store/actions/geoActions.js @@ -0,0 +1,15 @@ +import { getUserCurrentPosition } from '../../helpers/positionHelper'; +import { getLocationByCoords } from '../../Api/weatherApi'; +import { GEO_ACTIONS } from '../reducers/geoReducer'; + +const userGeoDataReceived = ({ position, location }) => ({ + type: GEO_ACTIONS.USER_GEO_DATA_RECEIVED, + payload: { position, location } +}); + +export const fetchUserGeoData = () => async dispatch => { + const position = await getUserCurrentPosition(); + const { longitude, latitude } = position.coords; + const location = await getLocationByCoords(longitude, latitude); + return dispatch(userGeoDataReceived({ position: position.coords, location })); +}; diff --git a/src/Store/reducers/geoReducer.js b/src/Store/reducers/geoReducer.js new file mode 100644 index 00000000..6ec20f69 --- /dev/null +++ b/src/Store/reducers/geoReducer.js @@ -0,0 +1,23 @@ +export const GEO_ACTIONS = { + USER_GEO_DATA_RECEIVED: 'USER_GEO_DATA_RECEIVED' +}; + +const initialState = { + position: null, + location: null, +}; + +export const geoReducer = (state = initialState, { type, payload }) => { + switch (type) { + case GEO_ACTIONS.USER_GEO_DATA_RECEIVED: + return { + ...state, + position: payload.position, + location: payload.location, + }; + default: + return state; + } +}; + +export default geoReducer; diff --git a/src/Store/selectors/geoSelectors.js b/src/Store/selectors/geoSelectors.js new file mode 100644 index 00000000..b7bdbe07 --- /dev/null +++ b/src/Store/selectors/geoSelectors.js @@ -0,0 +1,2 @@ +export const getLocation = state => state.geoData.location; +export const getPosition = state => state.geoData.position; diff --git a/src/Store/store.js b/src/Store/store.js new file mode 100644 index 00000000..c366ba19 --- /dev/null +++ b/src/Store/store.js @@ -0,0 +1,14 @@ +import { createStore, combineReducers, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; + +import homePageReducer from '../Components/Pages/HomePage/homePageReducer'; +import geoReducer from './reducers/geoReducer'; + +const rootReducer = combineReducers({ + homePage: homePageReducer, + geoData: geoReducer +}); + +const store = createStore(rootReducer, applyMiddleware(thunk)); + +export default store; diff --git a/src/helpers/formatHelper.js b/src/helpers/formatHelper.js new file mode 100644 index 00000000..84851794 --- /dev/null +++ b/src/helpers/formatHelper.js @@ -0,0 +1,7 @@ +export const capitalize = s => s && s[0].toUpperCase() + s.slice(1); +export const toReadableDate = time => new Date(time).toDateString(); +export const toReadableTime = time => + new Date(time).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + }); diff --git a/src/helpers/positionHelper.js b/src/helpers/positionHelper.js new file mode 100644 index 00000000..a4e39800 --- /dev/null +++ b/src/helpers/positionHelper.js @@ -0,0 +1,5 @@ +export async function getUserCurrentPosition() { + return new Promise((resolve, reject) => { + window.navigator.geolocation.getCurrentPosition(resolve, reject); + }); +} diff --git a/src/index.jsx b/src/index.jsx index c8f431f9..3b2210ce 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,16 +1,19 @@ import ReactDOM from 'react-dom'; import { BrowserRouter, Route } from 'react-router-dom'; import { Routes } from 'react-router'; +import store from './Store/store'; +import { Provider } from 'react-redux'; -import HomePage from './Components/HomePage/HomePage'; -import DetailedForecastPage from './Components/DetailedForesastPage/DetailedForecastPage'; -import FindLocation from './Components/FindLocationPage/FindLocationPage'; -import AboutPage from './Components/AboutPage/AboutPage'; -import ContactPage from './Components/ContactPage/ContactPage'; +import HomePage from './Components/Pages/HomePage/HomePage'; +import DetailedForecastPage from './Components/Pages/DetailedForesastPage/DetailedForecastPage'; +import FindLocation from './Components/Pages/FindLocationPage/FindLocationPage'; +import AboutPage from './Components/Pages/AboutPage/AboutPage'; +import ContactPage from './Components/Pages/ContactPage/ContactPage'; import NavigationPanel from './Components/NavigationPanel/NavigationPanel'; - ReactDOM.render( + +

HappyWeather

@@ -22,6 +25,8 @@ ReactDOM.render( } /> Invalid route!

} />
-
, + + +
, document.getElementById('app') ); diff --git a/webpack/webpack.dev.js b/webpack/webpack.dev.js index 5f25dc42..248c6cb0 100644 --- a/webpack/webpack.dev.js +++ b/webpack/webpack.dev.js @@ -1,6 +1,7 @@ const webpack = require('webpack'); const commonPaths = require('./paths'); +const Dotenv = require('dotenv-webpack'); module.exports = { mode: 'development', @@ -39,5 +40,10 @@ module.exports = { hot: true, port: 9020 }, - plugins: [new webpack.HotModuleReplacementPlugin()] + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new Dotenv({ + path: `../.env` + }) + ] };