Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ package-lock.json
/build/*
# Editor/IDE
.idea
packages/api/etc/
packages/api/etc/

# api values
.env
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>_<surname> created for you in original repository. Your task is to fork from this repo and work
There is a branch <name>\_<surname> 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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions src/Api/weatherApi.js
Original file line number Diff line number Diff line change
@@ -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
};
};
10 changes: 0 additions & 10 deletions src/Components/HomePage/HomePage.jsx

This file was deleted.

7 changes: 7 additions & 0 deletions src/Components/Loader/Loader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styles from './loader.css';

function Loader() {
return <div className={styles['loader']}></div>;
}

export default Loader;
9 changes: 9 additions & 0 deletions src/Components/Loader/loader.css
Original file line number Diff line number Diff line change
@@ -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%);
}
15 changes: 15 additions & 0 deletions src/Components/NavigationPanel/NavigationPanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Link } from 'react-router-dom';

function NavigationPanel() {
return (
<div>
<nav>
<Link to="/">Home</Link> | <Link to="detailedForecast">Detailed forecast</Link> |{' '}
<Link to="findLocation">Find location</Link> | <Link to="about">About</Link> |{' '}
<Link to="contact">Contact</Link>
</nav>
</div>
);
}

export default NavigationPanel;
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
function ContactPage() {

return (
<div>
<h1>Contact</h1>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
function DetailedForecastPage() {

return (
<div>
<h1>Detailed Page</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ function FindLocation() {
);
}

export default FindLocation;
export default FindLocation;
40 changes: 40 additions & 0 deletions src/Components/Pages/HomePage/HomePage.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h1>Home</h1>
{location && (
<h2>
{location.city}, {location.country}
</h2>
)}
{loading && <Loader />}
{currentWeather && <p>{toReadableDate(currentWeather.current.time)}</p>}
{currentWeather && <p>{toReadableTime(currentWeather.current.time)}</p>}
{currentWeather && <p>Probability of precipitation: {currentWeather.current.precipProb}%</p>}
{currentWeather && <p>Pressure (hPa) {currentWeather.current.pressure}%</p>}
{currentWeather && <p>Wind speed (m/s) {currentWeather.current.windSpeed}</p>}
{currentWeather && <p>{capitalize(currentWeather.current.symbolPhrase)}</p>}
{currentWeather && <SafeWeatherImage symbolCode={currentWeather.current.symbol} />}
</>
);
}

export default HomePage;
36 changes: 36 additions & 0 deletions src/Components/Pages/HomePage/homePageActions.js
Original file line number Diff line number Diff line change
@@ -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));
}
};
7 changes: 7 additions & 0 deletions src/Components/Pages/HomePage/homePageInitialState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const initialState = {
currentWeather: null,
loading: false,
error: null
};

export default initialState;
29 changes: 29 additions & 0 deletions src/Components/Pages/HomePage/homePageReducer.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/Components/Pages/HomePage/homePageSelectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const getCurrentWeather = state => state.homePage.currentWeather;
export const getLoading = state => state.homePage.loading;
18 changes: 18 additions & 0 deletions src/Components/SafeImage/SafeWeatherImage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useState } from 'react';

function SafeWeatherImage({ symbolCode }) {
const [isImageExist, setIsImageExist] = useState(true);

return (
isImageExist && (
<img
src={`https://developer.foreca.com/static/images/symbols/${symbolCode}.png`}
onError={() => {
setIsImageExist(false);
}}
/>
)
);
}

export default SafeWeatherImage;
15 changes: 15 additions & 0 deletions src/Store/actions/geoActions.js
Original file line number Diff line number Diff line change
@@ -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 }));
};
23 changes: 23 additions & 0 deletions src/Store/reducers/geoReducer.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/Store/selectors/geoSelectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const getLocation = state => state.geoData.location;
export const getPosition = state => state.geoData.position;
14 changes: 14 additions & 0 deletions src/Store/store.js
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 7 additions & 0 deletions src/helpers/formatHelper.js
Original file line number Diff line number Diff line change
@@ -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'
});
5 changes: 5 additions & 0 deletions src/helpers/positionHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export async function getUserCurrentPosition() {
return new Promise((resolve, reject) => {
window.navigator.geolocation.getCurrentPosition(resolve, reject);
});
}
19 changes: 12 additions & 7 deletions src/index.jsx
Original file line number Diff line number Diff line change
@@ -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(
<Provider store={store}>
<h2 style={{ color: 'blue' }}>HappyWeather</h2>
<BrowserRouter>
<NavigationPanel />
<Routes>
Expand All @@ -22,6 +25,8 @@ ReactDOM.render(
<Route path="contact" element={<ContactPage />} />
<Route path="*" element={<p>Invalid route!</p>} />
</Routes>
</BrowserRouter>,
</BrowserRouter>
<footer style={{ fontSize: '12px', marginTop: '10px' }}>© 2022 HappyWeather</footer>
</Provider>,
document.getElementById('app')
);
8 changes: 7 additions & 1 deletion webpack/webpack.dev.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const webpack = require('webpack');

const commonPaths = require('./paths');
const Dotenv = require('dotenv-webpack');

module.exports = {
mode: 'development',
Expand Down Expand Up @@ -39,5 +40,10 @@ module.exports = {
hot: true,
port: 9020
},
plugins: [new webpack.HotModuleReplacementPlugin()]
plugins: [
new webpack.HotModuleReplacementPlugin(),
new Dotenv({
path: `../.env`
})
]
};