diff --git a/.env b/.env new file mode 100644 index 00000000..af2371af --- /dev/null +++ b/.env @@ -0,0 +1 @@ +WEATHER_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9wZmEuZm9yZWNhLmNvbVwvYXV0aG9yaXplXC90b2tlbiIsImlhdCI6MTY2MjA0OTA3MiwiZXhwIjo5OTk5OTk5OTk5LCJuYmYiOjE2NjIwNDkwNzIsImp0aSI6ImE4NDRkZjZmZjA2MWY3N2UiLCJzdWIiOiJjb25jZW5jdWMiLCJmbXQiOiJYRGNPaGpDNDArQUxqbFlUdGpiT2lBPT0ifQ.myDTy0LZS__4k-Gjjz4jP_5ljikTDP4vEsrKzWC79oM \ No newline at end of file diff --git a/.gitignore b/.gitignore index b7925b9b..c64c6a3e 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 keys +.env \ No newline at end of file diff --git a/README.md b/README.md index 3af45433..90fa38f5 100644 --- a/README.md +++ b/README.md @@ -26,22 +26,21 @@ You are about to create weather application with location detection. ### Full description -Use weather API: https://developer.foreca.com/#Forecasts. Note that this API is **NOT** public. Your task is to create -front-end for that API. Your application should not have any login buttons to authenticate against weather api. It -should happen silently for the user. You need to create next parts (every page has a layout with a header, footer and +Use weather API: https://developer.foreca.com/#Forecasts. Note that this API is **NOT** public. Your task is to create +front-end for that API. Your application should not have any login buttons to authenticate against weather api. It +should happen silently for the user. You need to create next parts (every page has a layout with a header, footer and 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/babel.config.js b/babel.config.js index 6444f389..d09f4549 100644 --- a/babel.config.js +++ b/babel.config.js @@ -16,7 +16,7 @@ module.exports = function (api) { ['@babel/preset-react', { runtime: 'automatic' }] ]; const plugins = [ - '@babel/transform-react-constant-elements', + '@babel/transform-react-constant-elements', 'transform-react-pure-class-to-function', '@babel/plugin-transform-runtime', 'react-hot-loader/babel', diff --git a/backend.js b/backend.js new file mode 100644 index 00000000..36b77237 --- /dev/null +++ b/backend.js @@ -0,0 +1,85 @@ +const express = require('express'); +const cors = require('cors'); +const axios = require('axios'); +require('dotenv').config(); +const app = express(); + +app.use(cors()); + +const foreca = axios.create({ + baseURL: 'https://pfa.foreca.com/api/v1', + headers: { + authorization: `Bearer ${process.env.WEATHER_API_KEY}` + } +}); + +app.get('/', (req, res) => { + res.send('Homepage'); +}); + +app.get('/get-city', (req, res) => { + foreca.get(`location/${req.query.latitude},${req.query.latitude}`).then(function (response) { + res.json({ + country: response.data.country, + city: response.data.name, + latitude: response.data.lat, + longitude: response.data.lon + }); + }); +}); + +app.get('/get-city-coords', (req, res) => { + foreca.get(`location/search/${req.query.query}`).then(function (response) { + res.json({ + country: response.data.locations[0].country, + city: response.data.locations[0].name, + latitude: response.data.locations[0].lat, + longitude: response.data.locations[0].lon + }); + }); +}); + +app.get('/get-current-weather', (req, res) => { + foreca + .get(`current/location=${req.query.latitude},${req.query.longitude}`) + .then(function (response) { + res.json({ + country: req.query.country, + city: req.query.city, + symbol: response.data.current.symbol, + temperature: response.data.current.temperature, + relHumidity: response.data.current.relHumidity, + windSpeed: response.data.current.windSpeed, + cloudiness: response.data.current.cloudiness + }); + }); +}); + +app.get('/get-detail-weather', (req, res) => { + foreca + .get( + `forecast/daily/location=${req.query.latitude},${req.query.longitude}&dataset=full&periods=8` + ) + .then(function (response) { + let detailWeather = () => { + return response.data.forecast.map(item => { + return { + date: item.date, + symbol: item.symbol, + minTemp: item.minTemp, + maxTemp: item.maxTemp, + minRelHumidity: item.minRelHumidity, + maxRelHumidity: item.maxRelHumidity, + windSpeed: item.maxWindSpeed, + cloudiness: item.cloudiness + }; + }); + }; + + res.send(detailWeather()); + }); +}); + +app.listen(8000, () => { + console.log('server is runnings'); +}); diff --git a/package.json b/package.json index aa638dc9..151c6c1e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "start": "npm run format && cross-env NODE_ENV=development webpack-dev-server --open", + "start": "concurrently \"npm:start:backend\" \"npm:start:frontend\"", + "start:backend": "nodemon backend.js", + "start:frontend": "npm run format && cross-env NODE_ENV=development webpack-dev-server --open", "build": "cross-env NODE_ENV=production webpack", "test": "jest --coverage", "format": "prettier --write ." @@ -18,18 +20,29 @@ "npm": ">=3" }, "dependencies": { + "@reduxjs/toolkit": "^1.8.3", "axios": "0.21.4", + "concurrently": "^7.3.0", "core-js": "3.2.1", + "cors": "^2.8.5", + "country-state-city": "^3.0.1", + "dotenv": "^16.0.2", + "express": "^4.18.1", "history": "4.10.1", + "nodemon": "^2.0.19", + "npm-run-all": "^4.1.5", + "parallel": "^1.2.0", "prop-types": "15.7.2", "ramda": "0.26.1", "react": "17.0.1", "react-dom": "17.0.1", + "react-geolocated": "^4.0.3", "react-redux": "7.2.2", - "react-router-dom": "5.2.0", + "react-router-dom": "^6.3.0", "redux": "4.0.5", "redux-thunk": "2.3.0", - "reselect": "4.0.0" + "reselect": "4.0.0", + "webpack-php-loader": "^0.5.0" }, "resolutions": { "babel-core": "7.0.0-bridge.0" @@ -62,6 +75,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", @@ -70,13 +84,13 @@ "jsdom": "15.1.1", "koa-connect": "2.0.1", "mini-css-extract-plugin": "0.8.0", - "node-sass": "4.14.1", "npm-check-updates": "3.1.24", "optimize-css-assets-webpack-plugin": "5.0.3", "prettier": "2.2.1", "pretty-quick": "1.11.1", "react-hot-loader": "4.13.0", "redux-immutable-state-invariant": "2.1.0", + "sass": "1.52.3", "sass-loader": "8.0.0", "script-ext-html-webpack-plugin": "2.1.4", "style-loader": "1.0.0", diff --git a/src/Api/createWeatherApi.js b/src/Api/createWeatherApi.js new file mode 100644 index 00000000..c535b195 --- /dev/null +++ b/src/Api/createWeatherApi.js @@ -0,0 +1,9 @@ +import { TOKEN, MAIN_URL } from '../Config/constants'; +const axios = require('axios').default; + +export default axios.create({ + baseURL: MAIN_URL, + headers: { + authorization: `Bearer ${TOKEN}` + } +}); diff --git a/src/Api/getCity.js b/src/Api/getCity.js new file mode 100644 index 00000000..0772c25f --- /dev/null +++ b/src/Api/getCity.js @@ -0,0 +1,14 @@ +const axios = require('axios'); +import { backendURL } from '../Config/constants'; + +const getCity = async coords => { + const options = { + method: 'GET', + url: backendURL + '/get-city', + params: coords + }; + + return await axios.request(options).then(response => response.data); +}; + +export default getCity; diff --git a/src/Api/getCityCoords.js b/src/Api/getCityCoords.js new file mode 100644 index 00000000..d3b227c3 --- /dev/null +++ b/src/Api/getCityCoords.js @@ -0,0 +1,14 @@ +const axios = require('axios'); +import { backendURL } from '../Config/constants'; + +const getCityCoords = async query => { + const options = { + method: 'GET', + url: backendURL + '/get-city-coords', + params: { query: query } + }; + + return await axios.request(options).then(response => response.data); +}; + +export default getCityCoords; diff --git a/src/Api/getCurrentWeather.js b/src/Api/getCurrentWeather.js new file mode 100644 index 00000000..5adba89b --- /dev/null +++ b/src/Api/getCurrentWeather.js @@ -0,0 +1,14 @@ +const axios = require('axios'); +import { backendURL } from '../Config/constants'; + +const getCurrentWeather = async coords => { + const options = { + method: 'GET', + url: backendURL + '/get-current-weather', + params: coords + }; + + return await axios.request(options).then(response => response.data); +}; + +export default getCurrentWeather; diff --git a/src/Api/getDetailWeather.js b/src/Api/getDetailWeather.js new file mode 100644 index 00000000..09d50fc7 --- /dev/null +++ b/src/Api/getDetailWeather.js @@ -0,0 +1,14 @@ +const axios = require('axios'); +import { backendURL } from '../Config/constants'; + +const getDetailWeather = async coords => { + const options = { + method: 'GET', + url: backendURL + '/get-detail-weather', + params: coords + }; + + return await axios.request(options).then(response => response.data); +}; + +export default getDetailWeather; diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 084b3f2f..00000000 --- a/src/App.jsx +++ /dev/null @@ -1,5 +0,0 @@ -function App() { - return
Hello world!
; -} - -export default App; diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 80b67e1e..00000000 --- a/src/App.test.js +++ /dev/null @@ -1,3 +0,0 @@ -test('renders learn react link', () => { - expect(true).toBe(true); -}); diff --git a/src/Components/CitiesForm/CitiesForm.css b/src/Components/CitiesForm/CitiesForm.css new file mode 100644 index 00000000..efab423d --- /dev/null +++ b/src/Components/CitiesForm/CitiesForm.css @@ -0,0 +1,62 @@ +.form { + margin-top: 30px; + margin-right: 50px; + width: 350px; + display: flex; + flex-wrap: wrap; +} +.selectWrap { + margin-bottom: 20px; + position: relative; + width: 100%; +} +.selectWrap:after { + content: ''; + position: absolute; + right: 25px; + top: 19px; + width: 13px; + height: 13px; + transform: rotate(45deg); + border: 3px solid var(--text-primary); + border-top: none; + border-left: none; +} +.select { + cursor: pointer; + border: 3px solid #3cc2ff; + border-radius: 30px; + display: inline-flex; + padding: 0 20px; + height: 54px; + line-height: 48px; + width: 100%; +} +.inputWrap { + width: 100%; + border: 3px solid #3cc2ff; + border-radius: 30px; + display: inline-flex; + padding: 6px; + padding-left: 20px; +} +.input { + padding-right: 20px; + width: 100%; +} +.btn { + display: inline-block; + padding: 0 20px; + border-radius: 20px; + height: 40px; + line-height: 40px; + font-weight: 500; + font-size: 20px; + text-align: center; + background-color: #3cc2ff; + color: #fff; +} +.btn:hover { + transition: 0.2s; + background-color: #3095cb; +} diff --git a/src/Components/CitiesForm/CitiesForm.js b/src/Components/CitiesForm/CitiesForm.js new file mode 100644 index 00000000..42f067ca --- /dev/null +++ b/src/Components/CitiesForm/CitiesForm.js @@ -0,0 +1,64 @@ +import { useState } from 'react'; + +import styles from './CitiesForm.css'; + +import { Country, City } from 'country-state-city'; + +const CitiesForm = props => { + const countries = Country.getAllCountries(); + + return ( +
+
+ +
+ {props.currentCountry && ( +
+ +
+ )} +
+ + +
+
+ ); +}; + +export default CitiesForm; diff --git a/src/Components/City/City.css b/src/Components/City/City.css new file mode 100644 index 00000000..38e37676 --- /dev/null +++ b/src/Components/City/City.css @@ -0,0 +1,52 @@ +.city { + display: flex; + flex-wrap: wrap; +} +.symbol { + margin-right: 20px; + height: 70px; +} +h2 { + font-weight: 700; + font-size: 30px; + color: #3cc2ff; +} +h3 { + margin-top: 10px; + font-weight: 500; + font-size: 24px; +} +h4 { + color: #858584; +} +.list { + margin-left: 30px; +} +.list li { + margin-top: 6px; +} +.list li b { + font-size: 20px; +} +.list li:first-child { + margin-top: 0; +} +.more { + transition: 0.2s; + cursor: pointer; + margin-left: 16px; + display: inline-block; + padding: 0 20px; + border-radius: 20px; + height: 40px; + line-height: 40px; + font-weight: 500; + font-size: 20px; + text-align: center; + background-color: #3cc2ff; + color: #fff; +} +.more:hover { + transition: 0.2s; + background-color: #3095cb; +} diff --git a/src/Components/City/City.js b/src/Components/City/City.js new file mode 100644 index 00000000..c02f9b84 --- /dev/null +++ b/src/Components/City/City.js @@ -0,0 +1,43 @@ +import styles from './City.css'; + +import { useNavigate } from 'react-router-dom'; + +const City = props => { + const navigate = useNavigate(); + + const viewDetails = () => { + navigate(`/city&city=${props.weather.city}`); + }; + + return ( + <> +
+ +
+

{props.weather.country}

+

{props.weather.city}

+

{props.weather.temperature}°C

+
+
    +
  • + Humidity: {props.weather.relHumidity}% +
  • +
  • + Cloudiness: {props.weather.cloudiness} +
  • +
  • + Wind speed: {props.weather.windSpeed} м/с +
  • +
+
+ Details +
+
+ + ); +}; + +export default City; diff --git a/src/Components/CityDetails/CityDetails.css b/src/Components/CityDetails/CityDetails.css new file mode 100644 index 00000000..69cc2a25 --- /dev/null +++ b/src/Components/CityDetails/CityDetails.css @@ -0,0 +1,57 @@ +.prevPage { + font-size: 24px; + cursor: pointer; +} +.prevPage:before { + content: '<-'; + margin-right: 10px; + display: inline; +} +.CityDetailsWrap { + width: 100%; + display: flex; + flex-wrap: wrap; +} +.city { + width: 33.33%; + display: flex; + flex-wrap: wrap; + margin-top: 30px; +} +.cityLarge { + margin-bottom: 30px; + zoom: 1.4; + width: 50%; +} +.symbol { + margin-right: 20px; + height: 70px; +} +.h2 { + margin-top: 10px; + width: 100%; + margin: 20px 0; + font-size: 36px; +} +.h3 { + margin-top: 0; + font-weight: 500; + font-size: 26px; +} +.h4 { + margin-top: 15px; + font-weight: 500; + font-size: 24px; +} +.list { + margin-left: 20px; +} +.list li { + margin-top: 6px; +} +.list li b { + font-size: 18px; +} +.list li:first-child { + margin-top: 0; +} diff --git a/src/Components/CityDetails/CityDetails.js b/src/Components/CityDetails/CityDetails.js new file mode 100644 index 00000000..544f2ce8 --- /dev/null +++ b/src/Components/CityDetails/CityDetails.js @@ -0,0 +1,24 @@ +import styles from './CityDetails.css'; +import { useNavigate } from 'react-router-dom'; +import WeeklyWeather from '../WeeklyWeather/WeeklyWeather'; + +const CityDetails = props => { + const navigate = useNavigate(); + const onPrevPage = () => { + navigate(-1); + }; + + return ( + <> +
+ Previus page +
+

{props.city}

+
+ +
+ + ); +}; + +export default CityDetails; diff --git a/src/Components/ContactsForm/ContactsForm.css b/src/Components/ContactsForm/ContactsForm.css new file mode 100644 index 00000000..2e566308 --- /dev/null +++ b/src/Components/ContactsForm/ContactsForm.css @@ -0,0 +1,103 @@ +.form { + margin-top: 30px; + margin-right: 50px; + width: 500px; + display: flex; + flex-wrap: wrap; +} +.inputWrap { + margin-bottom: 30px; + width: 100%; + border: 3px solid #3cc2ff; + border-radius: 30px; + display: inline-flex; + padding: 6px 20px; +} +.inputWrap.invalid { + border-color: #f82222; +} +.input { + width: 100%; + height: 40px; + line-height: 40px; +} +textarea.input { + padding-top: 10px; + padding-bottom: 10px; + line-height: 26px; + height: 150px; +} +.checkboxWrap { + cursor: pointer; + margin-bottom: 30px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; +} +.checkboxWrap .checkboxEl { + content: ''; + display: flex; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + border: 3px solid #3cc2ff; + border-radius: 8px; +} +.checkboxWrap .checkboxEl:after { + transition: 0.2s; + opacity: 0; + content: ''; + width: 10px; + height: 10px; + border-radius: 2px; + background-color: #3cc2ff; +} +.checkboxWrap input:checked ~ .checkboxEl:after { + transition: 0.2s; + opacity: 1; +} +.selectWrap { + margin-bottom: 30px; + position: relative; + width: 100%; +} +.selectWrap:after { + content: ''; + position: absolute; + right: 25px; + top: 19px; + width: 13px; + height: 13px; + transform: rotate(45deg); + border: 3px solid var(--text-primary); + border-top: none; + border-left: none; +} +.select { + cursor: pointer; + border: 3px solid #3cc2ff; + border-radius: 30px; + display: inline-flex; + padding: 0 20px; + height: 54px; + line-height: 48px; + width: 100%; +} +.btn { + display: inline-block; + padding: 0 40px; + border-radius: 28px; + height: 50px; + line-height: 50px; + font-weight: 500; + font-size: 20px; + text-align: center; + background-color: #3cc2ff; + color: #fff; +} +.btn:hover { + transition: 0.2s; + background-color: #3095cb; +} diff --git a/src/Components/ContactsForm/ContactsForm.js b/src/Components/ContactsForm/ContactsForm.js new file mode 100644 index 00000000..00d7c707 --- /dev/null +++ b/src/Components/ContactsForm/ContactsForm.js @@ -0,0 +1,127 @@ +import styles from './ContactsForm.css'; +import { useDispatch, useSelector } from 'react-redux'; + +import contactFormRequest from '../../Services/mailers/contactFormRequest'; + +import { contactsFormActions } from '../../Store/reducers/ContactsFormSlice/index'; +import { popupActions } from '../../Store/reducers/PopupSlice/index'; + +import PopupPortal from '../../Components/Popup/PopupPortal'; + +const ContactsForm = () => { + const dispatch = useDispatch(); + const { + name, + nameIsValid, + email, + emailIsValid, + message, + question1, + question2, + question3 + } = useSelector(state => state.contactsForm); + const { popupMessage } = useSelector(state => state.popup); + + const nameChangeHandler = e => dispatch(contactsFormActions.changeName(e.target.value)); + const emailChangeHandler = e => dispatch(contactsFormActions.changeEmail(e.target.value)); + const messageChangeHandler = e => dispatch(contactsFormActions.changeMessage(e.target.value)); + const question1ChangeHandler = e => dispatch(contactsFormActions.changeQuestion1(e.target.value)); + const question2ChangeHandler = () => dispatch(contactsFormActions.changeQuestion2(!question2)); + const question3ChangeHandler = e => dispatch(contactsFormActions.changeQuestion3(e.target.value)); + + const submitForm = e => { + e.preventDefault(); + + if (!nameIsValid) { + dispatch(popupActions.setMessage('Name is invalid!')); + } else if (!emailIsValid) { + dispatch(popupActions.setMessage('Email is invalid!')); + } else { + contactFormRequest({ name, email, message, question1, question2, question3 }) + .then(() => { + dispatch(popupActions.setMessage('Thank you for application!')); + }) + .catch(error => { + console.log(error); + dispatch(popupActions.setMessage('Something was wrong!')); + }); + } + }; + + return ( + <> + {popupMessage && } +
+
+ +
+
+ +
+
+ +
+ + +
+ +
+
+ +
+ +
+ + ); +}; + +export default ContactsForm; diff --git a/src/Components/Footer/Footer.css b/src/Components/Footer/Footer.css new file mode 100644 index 00000000..f8604af8 --- /dev/null +++ b/src/Components/Footer/Footer.css @@ -0,0 +1,29 @@ +.footer { + margin-top: auto; + padding: 40px; + background-color: var(--footer-background); +} +.container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + position: relative; + width: 100%; + height: 100%; + max-width: 1460px; + padding-right: 30px; + padding-left: 30px; + margin-right: auto; + margin-left: auto; +} +.footer a { + transition: 0.2s; +} +.footer a:hover { + transition: 0.2s; + opacity: 0.8; +} diff --git a/src/Components/Footer/Footer.js b/src/Components/Footer/Footer.js new file mode 100644 index 00000000..9ea53b2b --- /dev/null +++ b/src/Components/Footer/Footer.js @@ -0,0 +1,19 @@ +import styles from './Footer.css'; + +const Header = () => { + return ( + + ); +}; + +export default Header; diff --git a/src/Components/Header/Header.css b/src/Components/Header/Header.css new file mode 100644 index 00000000..a0046a3f --- /dev/null +++ b/src/Components/Header/Header.css @@ -0,0 +1,59 @@ +header { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 120px; + border-bottom: 1px solid #fff; + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); +} +header:before { + z-index: -1; + content: ''; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + display: block; + background-color: #fff; + opacity: 0.4; +} +.container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + align-items: center; + position: relative; + width: 100%; + height: 100%; + max-width: 1460px; + padding-right: 30px; + padding-left: 30px; + margin-right: auto; + margin-left: auto; +} +header nav { + height: 100%; + display: flex; + align-items: center; +} +header ul { + display: flex; + align-items: center; +} +header li { + margin-right: 20px; + list-style-type: none; +} +header a { + font-weight: 700; + font-size: 20px; + margin-right: 40px; +} +header a:hover { + opacity: 0.8; +} diff --git a/src/Components/Header/Header.js b/src/Components/Header/Header.js new file mode 100644 index 00000000..146e0710 --- /dev/null +++ b/src/Components/Header/Header.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import styles from './Header.css'; + +import ThemeBtn from '../ThemeBtn/ThemeBtn'; + +const Header = () => { + return ( +
+
+ + +
+
+ ); +}; + +export default Header; diff --git a/src/Components/Popup/Popup.css b/src/Components/Popup/Popup.css new file mode 100644 index 00000000..eb6e74ff --- /dev/null +++ b/src/Components/Popup/Popup.css @@ -0,0 +1,30 @@ +.popupWrap { + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; +} +.popup { + position: relative; + width: 400px; + max-width: calc(100% - 40px); + background-color: #fff; + padding: 50px 35px; +} +.close { + cursor: pointer; + position: absolute; + transform: rotate(45deg); + right: 10px; + top: 6px; + font-weight: 700; + font-size: 40px; +} +.message { + font-size: 26px; +} diff --git a/src/Components/Popup/Popup.js b/src/Components/Popup/Popup.js new file mode 100644 index 00000000..f43b43e6 --- /dev/null +++ b/src/Components/Popup/Popup.js @@ -0,0 +1,26 @@ +import styles from './Popup.css'; +import { useDispatch } from 'react-redux'; +import { popupActions } from '../../Store/reducers/PopupSlice/index'; + +const Popup = props => { + const dispatch = useDispatch(); + + const closePopup = () => { + dispatch(popupActions.setMessage(null)); + }; + return ( + <> +
+
+
+ + +
+
{props.message}
+
+
+
+ + ); +}; + +export default Popup; diff --git a/src/Components/Popup/PopupPortal.js b/src/Components/Popup/PopupPortal.js new file mode 100644 index 00000000..70b4f485 --- /dev/null +++ b/src/Components/Popup/PopupPortal.js @@ -0,0 +1,8 @@ +import Popup from './Popup'; +import * as _ReactDOM from 'react-dom'; + +const portalPopup = props => { + return _ReactDOM.createPortal(, document.querySelector('body')); +}; + +export default portalPopup; diff --git a/src/Components/Section/Section.css b/src/Components/Section/Section.css new file mode 100644 index 00000000..46b7e677 --- /dev/null +++ b/src/Components/Section/Section.css @@ -0,0 +1,29 @@ +section { + margin-bottom: 60px; + width: 100%; +} +.container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + position: relative; + width: 100%; + height: 100%; + max-width: 1460px; + padding-right: 30px; + padding-left: 30px; + margin-right: auto; + margin-left: auto; +} +.block { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + width: 100%; + background-color: var(--background); + padding: 50px 40px; +} diff --git a/src/Components/Section/Section.js b/src/Components/Section/Section.js new file mode 100644 index 00000000..2c0ae2d9 --- /dev/null +++ b/src/Components/Section/Section.js @@ -0,0 +1,13 @@ +import styles from './Section.css'; + +const Section = props => { + return ( +
+
+
{props.children}
+
+
+ ); +}; + +export default Section; diff --git a/src/Components/ThemeBtn/ThemeBtn.css b/src/Components/ThemeBtn/ThemeBtn.css new file mode 100644 index 00000000..9f43f8be --- /dev/null +++ b/src/Components/ThemeBtn/ThemeBtn.css @@ -0,0 +1,21 @@ +.themeBtn { + cursor: pointer; + display: flex; + padding: 5px; + margin-left: auto; + width: 50px; + border-radius: 20px; + border: 2px solid #3cc2ff; +} +.themeBtn:before { + transition: 0.2s; + content: ''; + width: 20px; + height: 20px; + background-color: #3cc2ff; + border-radius: 50%; +} +.themeBtn.darkTheme:before { + transition: 0.2s; + transform: translateX(13px); +} diff --git a/src/Components/ThemeBtn/ThemeBtn.js b/src/Components/ThemeBtn/ThemeBtn.js new file mode 100644 index 00000000..2636965f --- /dev/null +++ b/src/Components/ThemeBtn/ThemeBtn.js @@ -0,0 +1,26 @@ +import styles from './themeBtn.css'; + +import { useDispatch, useSelector } from 'react-redux'; +import { themeActions } from '../../Store/reducers/ThemeSlice/index'; + +import themeStorage from '../../Services/ThemeStorage'; + +const ThemeBtn = () => { + const dispatch = useDispatch(); + const { theme } = useSelector(state => state.theme); + + const changeTheme = () => { + let newTheme = theme === 'light' ? 'dark' : 'light'; + dispatch(themeActions.setTheme(newTheme)); + themeStorage().setTheme(newTheme); + }; + + return ( +
+ ); +}; + +export default ThemeBtn; diff --git a/src/Components/ThemeWrapper/ThemeWrapper.css b/src/Components/ThemeWrapper/ThemeWrapper.css new file mode 100644 index 00000000..e69de29b diff --git a/src/Components/ThemeWrapper/ThemeWrapper.js b/src/Components/ThemeWrapper/ThemeWrapper.js new file mode 100644 index 00000000..38232c14 --- /dev/null +++ b/src/Components/ThemeWrapper/ThemeWrapper.js @@ -0,0 +1,20 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useEffect } from 'react'; + +import themeStorage from '../../Services/ThemeStorage'; + +import { themeActions } from '../../Store/reducers/ThemeSlice/index'; + +const ThemeWrapper = props => { + const dispatch = useDispatch(); + const { theme } = useSelector(state => state.theme); + + useEffect(() => { + const getTheme = themeStorage().getTheme(); + dispatch(themeActions.setTheme(getTheme)); + }, []); + + return
{props.children}
; +}; + +export default ThemeWrapper; diff --git a/src/Components/WeeklyWeather/WeeklyWeather.css b/src/Components/WeeklyWeather/WeeklyWeather.css new file mode 100644 index 00000000..69cc2a25 --- /dev/null +++ b/src/Components/WeeklyWeather/WeeklyWeather.css @@ -0,0 +1,57 @@ +.prevPage { + font-size: 24px; + cursor: pointer; +} +.prevPage:before { + content: '<-'; + margin-right: 10px; + display: inline; +} +.CityDetailsWrap { + width: 100%; + display: flex; + flex-wrap: wrap; +} +.city { + width: 33.33%; + display: flex; + flex-wrap: wrap; + margin-top: 30px; +} +.cityLarge { + margin-bottom: 30px; + zoom: 1.4; + width: 50%; +} +.symbol { + margin-right: 20px; + height: 70px; +} +.h2 { + margin-top: 10px; + width: 100%; + margin: 20px 0; + font-size: 36px; +} +.h3 { + margin-top: 0; + font-weight: 500; + font-size: 26px; +} +.h4 { + margin-top: 15px; + font-weight: 500; + font-size: 24px; +} +.list { + margin-left: 20px; +} +.list li { + margin-top: 6px; +} +.list li b { + font-size: 18px; +} +.list li:first-child { + margin-top: 0; +} diff --git a/src/Components/WeeklyWeather/WeeklyWeather.js b/src/Components/WeeklyWeather/WeeklyWeather.js new file mode 100644 index 00000000..06e32155 --- /dev/null +++ b/src/Components/WeeklyWeather/WeeklyWeather.js @@ -0,0 +1,9 @@ +import WeeklyWeatherItem from './WeeklyWeatherItem/WeeklyWeatherItem'; + +const WeeklyWeather = props => { + return props.weather.map((item, index) => { + return ; + }); +}; + +export default WeeklyWeather; diff --git a/src/Components/WeeklyWeather/WeeklyWeatherItem/WeeklyWeatherItem.css b/src/Components/WeeklyWeather/WeeklyWeatherItem/WeeklyWeatherItem.css new file mode 100644 index 00000000..69cc2a25 --- /dev/null +++ b/src/Components/WeeklyWeather/WeeklyWeatherItem/WeeklyWeatherItem.css @@ -0,0 +1,57 @@ +.prevPage { + font-size: 24px; + cursor: pointer; +} +.prevPage:before { + content: '<-'; + margin-right: 10px; + display: inline; +} +.CityDetailsWrap { + width: 100%; + display: flex; + flex-wrap: wrap; +} +.city { + width: 33.33%; + display: flex; + flex-wrap: wrap; + margin-top: 30px; +} +.cityLarge { + margin-bottom: 30px; + zoom: 1.4; + width: 50%; +} +.symbol { + margin-right: 20px; + height: 70px; +} +.h2 { + margin-top: 10px; + width: 100%; + margin: 20px 0; + font-size: 36px; +} +.h3 { + margin-top: 0; + font-weight: 500; + font-size: 26px; +} +.h4 { + margin-top: 15px; + font-weight: 500; + font-size: 24px; +} +.list { + margin-left: 20px; +} +.list li { + margin-top: 6px; +} +.list li b { + font-size: 18px; +} +.list li:first-child { + margin-top: 0; +} diff --git a/src/Components/WeeklyWeather/WeeklyWeatherItem/WeeklyWeatherItem.js b/src/Components/WeeklyWeather/WeeklyWeatherItem/WeeklyWeatherItem.js new file mode 100644 index 00000000..6a3d94b3 --- /dev/null +++ b/src/Components/WeeklyWeather/WeeklyWeatherItem/WeeklyWeatherItem.js @@ -0,0 +1,44 @@ +import styles from './WeeklyWeatherItem.css'; + +const WeeklyWeatherItem = props => { + const change = id => { + let classLarge = '', + date = props.item.date; + if (id == 0) { + classLarge = styles.cityLarge; + date = 'Today'; + } else if (id == 1) { + classLarge = styles.cityLarge; + date = 'Tommorow'; + } + return { classLarge, date }; + }; + + return ( +
+ +
+

{change(props.id).date}

+

+ {props.item.minTemp}-{props.item.maxTemp}°C +

+
+
    +
  • + Humidity: {props.item.minRelHumidity}-{props.item.maxRelHumidity}% +
  • +
  • + Cloudiness: {props.item.cloudiness} +
  • +
  • + Wind speed: {props.item.windSpeed} м/с +
  • +
+
+ ); +}; + +export default WeeklyWeatherItem; diff --git a/src/Config/constants.js b/src/Config/constants.js new file mode 100644 index 00000000..5ee6e479 --- /dev/null +++ b/src/Config/constants.js @@ -0,0 +1 @@ +export const backendURL = 'http://localhost:8000'; diff --git a/src/Pages/CityPage.js b/src/Pages/CityPage.js new file mode 100644 index 00000000..3e4dbac3 --- /dev/null +++ b/src/Pages/CityPage.js @@ -0,0 +1,53 @@ +import { useLocation } from 'react-router-dom'; +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { cityPageDetailsActions } from '../Store/reducers/CityPageDetailsSlice/index'; +import { popupActions } from '../Store/reducers/PopupSlice/index'; + +import getCityCoords from '../Api/getCityCoords'; +import getDetailWeather from '../Api/getDetailWeather'; + +import Section from '../Components/Section/Section'; +import PopupPortal from '../Components/Popup/PopupPortal'; + +import CityDetails from '../Components/CityDetails/CityDetails'; + +const CityPage = () => { + const dispatch = useDispatch(); + const { city, cityDetails } = useSelector(state => state.cityPageDetails); + const { popupMessage } = useSelector(state => state.popup); + + let location = useLocation(); + + useEffect(() => { + dispatch(cityPageDetailsActions.setCity(decodeURI(location.pathname.split('=')[1]))); + }, [location]); + + useEffect(() => { + if (city) { + getCityCoords(city) + .then(data => { + getDetailWeather({ latitude: data.latitude, longitude: data.longitude }) + .then(data => { + dispatch(cityPageDetailsActions.setCityDetails(data)); + }) + .catch(() => { + dispatch(popupActions.setMessage('Сity is not found!')); + }); + }) + .catch(() => { + dispatch(popupActions.setMessage('Сity is not found!')); + }); + } + }, [city]); + + return ( + <> + {popupMessage && } +
{cityDetails && }
+ + ); +}; + +export default CityPage; diff --git a/src/Pages/Contacts.js b/src/Pages/Contacts.js new file mode 100644 index 00000000..6bfe3add --- /dev/null +++ b/src/Pages/Contacts.js @@ -0,0 +1,13 @@ +import Section from '../Components/Section/Section'; +import ContactsForm from '../Components/ContactsForm/ContactsForm'; + +const Contacts = () => { + return ( +
+

Contact Us

+ +
+ ); +}; + +export default Contacts; diff --git a/src/Pages/Homepage.js b/src/Pages/Homepage.js new file mode 100644 index 00000000..b6944363 --- /dev/null +++ b/src/Pages/Homepage.js @@ -0,0 +1,54 @@ +import Section from '../Components/Section/Section'; +import PopupPortal from '../Components/Popup/PopupPortal'; +import City from '../Components/City/City'; + +import { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { popupActions } from '../Store/reducers/PopupSlice/index'; + +import { useGeolocated } from 'react-geolocated'; +import getCurrentWeather from '../Api/getCurrentWeather'; +import getCity from '../Api/getCity'; + +const Homepage = () => { + const dispatch = useDispatch(); + const { popupMessage } = useSelector(state => state.popup); + + const { coords, isGeolocationAvailable, isGeolocationEnabled } = useGeolocated({ + positionOptions: { + enableHighAccuracy: false + }, + userDecisionTimeout: 1000 + }); + + const [currentWeather, setCurrentWeather] = useState(null); + + // getting City name by coords + useEffect(() => { + if (isGeolocationAvailable && isGeolocationEnabled && coords) { + getCity({ latitude: coords.latitude, longitude: coords.longitude }) + .then(data => { + getCurrentWeather(data) + .then(data => { + setCurrentWeather(data); + }) + .catch(() => { + dispatch(popupActions.setMessage('Сity is not found!')); + }); + }) + .catch(() => { + dispatch(popupActions.setMessage('Сity is not found!')); + }); + } + }, [coords]); + + return ( + <> + {popupMessage && } +
{currentWeather && }
+ + ); +}; + +export default Homepage; diff --git a/src/Pages/Info.js b/src/Pages/Info.js new file mode 100644 index 00000000..73278f27 --- /dev/null +++ b/src/Pages/Info.js @@ -0,0 +1,81 @@ +import Section from '../Components/Section/Section'; + +const Info = () => { + return ( +
+

Hello word!

+

+ Hello word - is a Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do{' '} + eiusmod tempor incididunt ut labore et dolore magna aliqua. Aenean euismod elementum nisi + quis eleifend quam adipiscing vitae. Est velit egestas dui id ornare arcu odio ut. Maecenas + sed enim ut sem viverra aliquet. Ullamcorper malesuada proin libero nunc consequat. Pharetra + diam sit amet nisl suscipit. Sed egestas egestas fringilla phasellus faucibus scelerisque + eleifend donec. Sit amet consectetur adipiscing elit ut aliquam purus sit. Fusce ut placerat + orci nulla pellentesque dignissim enim sit amet. Felis bibendum ut tristique et egestas quis + ipsum suspendisse. +

+

+ Arcu non sodales neque sodales ut etiam sit amet. Sed odio morbi quis commodo odio aenean. + Tellus pellentesque eu tincidunt tortor aliquam. Pellentesque habitant morbi tristique + senectus et netus et malesuada fames. Id volutpat lacus laoreet non curabitur. Eget gravida + cum sociis natoque penatibus et magnis dis. Facilisis mauris sit amet massa vitae. Sit amet + venenatis urna cursus eget nunc scelerisque viverra mauris. Enim blandit volutpat maecenas + volutpat blandit. Tellus elementum sagittis vitae et leo duis ut diam quam. Vulputate eu + scelerisque felis imperdiet proin fermentum leo. +

+

+ Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Senectus et netus et + malesuada fames ac turpis egestas. Feugiat in ante metus dictum at tempor commodo. In ante + metus dictum at tempor. Et netus et malesuada fames ac. Vulputate odio ut enim blandit + volutpat maecenas volutpat blandit aliquam. Congue quisque egestas diam in arcu cursus + euismod quis viverra. Etiam erat velit scelerisque in dictum. Est lorem ipsum dolor sit amet + consectetur adipiscing. Condimentum vitae sapien pellentesque habitant morbi tristique + senectus et. Aliquet sagittis id consectetur purus ut faucibus. +

+

+ Mauris in aliquam sem fringilla ut. Sit amet risus nullam eget. A lacus vestibulum sed arcu + non. Tempor orci dapibus ultrices in iaculis nunc. Diam ut venenatis tellus in metus + vulputate eu scelerisque. Phasellus faucibus scelerisque eleifend donec pretium vulputate + sapien nec sagittis. Amet mauris commodo quis imperdiet massa tincidunt nunc. Laoreet + suspendisse interdum consectetur libero id faucibus nisl tincidunt. Cursus metus aliquam + eleifend mi in nulla posuere sollicitudin aliquam. Id diam vel quam elementum pulvinar etiam + non quam. Adipiscing elit duis tristique sollicitudin nibh sit. Ac turpis egestas integer + eget aliquet. Justo eget magna fermentum iaculis. A iaculis at erat pellentesque adipiscing + commodo elit. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada + bibendum. Facilisi nullam vehicula ipsum a arcu cursus. Elit sed vulputate mi sit amet + mauris commodo. +

+

+ Hendrerit gravida rutrum quisque non tellus orci. Suspendisse sed nisi lacus sed. Ornare + aenean euismod elementum nisi quis eleifend quam adipiscing vitae. Adipiscing elit ut + aliquam purus sit amet luctus venenatis lectus. Nisl nunc mi ipsum faucibus. Quisque egestas + diam in arcu cursus euismod quis viverra nibh. Pulvinar pellentesque habitant morbi + tristique senectus. Pretium fusce id velit ut tortor pretium. Risus commodo viverra maecenas + accumsan. Et tortor at risus viverra adipiscing. In massa tempor nec feugiat nisl pretium + fusce. Etiam tempor orci eu lobortis elementum nibh tellus molestie nunc. Aliquam sem + fringilla ut morbi tincidunt augue. Interdum posuere lorem ipsum dolor sit amet consectetur + adipiscing elit. Mi eget mauris pharetra et ultrices neque ornare. Feugiat nisl pretium + fusce id. Ut eu sem integer vitae justo eget magna. +

+

+ Senectus et netus et malesuada. Quam nulla porttitor massa id neque aliquam. Ut venenatis + tellus in metus vulputate eu scelerisque. Ullamcorper dignissim cras tincidunt lobortis + feugiat vivamus at augue eget. Posuere ac ut consequat semper viverra nam. Vestibulum lorem + sed risus ultricies tristique nulla aliquet enim tortor. Faucibus interdum posuere lorem + ipsum dolor sit amet consectetur adipiscing. Pellentesque eu tincidunt tortor aliquam nulla + facilisi cras fermentum. Eu augue ut lectus arcu. Sit amet consectetur adipiscing elit ut + aliquam purus sit amet. Et sollicitudin ac orci phasellus egestas tellus. +

+

+ Quis varius quam quisque id diam vel quam elementum. Hendrerit gravida rutrum quisque non. + Enim sit amet venenatis urna cursus eget. Augue interdum velit euismod in pellentesque massa + placerat. Morbi enim nunc faucibus a. Duis at tellus at urna condimentum mattis pellentesque + id. Risus nec feugiat in fermentum posuere. Feugiat sed lectus vestibulum mattis ullamcorper + velit. Etiam tempor orci eu lobortis elementum nibh. Ut eu sem integer vitae justo. Eget + nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. +

+
+ ); +}; + +export default Info; diff --git a/src/Pages/List.js b/src/Pages/List.js new file mode 100644 index 00000000..91b1cb0e --- /dev/null +++ b/src/Pages/List.js @@ -0,0 +1,70 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import Section from '../Components/Section/Section'; +import PopupPortal from '../Components/Popup/PopupPortal'; + +import CitiesForm from '../Components/CitiesForm/CitiesForm'; +import City from '../Components/City/City'; + +import getCityCoords from '../Api/getCityCoords'; +import getCurrentWeather from '../Api/getCurrentWeather'; + +import { listPageActions } from '../Store/reducers/ListPageSlice/index'; +import { popupActions } from '../Store/reducers/PopupSlice/index'; + +const List = () => { + const dispatch = useDispatch(); + const { currentCountry, city, cityWeather } = useSelector(state => state.listPage); + const { popupMessage } = useSelector(state => state.popup); + + const getCity = e => { + dispatch(listPageActions.setCity(e.target.value)); + }; + + const getCurrentCountry = e => { + dispatch(listPageActions.setCurrentCountry(e.target.value)); + }; + + const submitForm = e => { + e.preventDefault(); + + if (city) { + getCityCoords(city) + .then(data => { + if (data) { + getCurrentWeather(data) + .then(data => { + dispatch(listPageActions.setCityWeather(data)); + }) + .catch(() => { + dispatch(popupActions.setMessage('Сity is not found!')); + }); + } else { + dispatch(popupActions.setMessage('Сity is not found!')); + } + }) + .catch(() => { + dispatch(popupActions.setMessage('Сity is not found!')); + }); + } + }; + + return ( + <> + {popupMessage && } +
+

Search city

+ + {cityWeather && } +
+ + ); +}; + +export default List; diff --git a/src/Services/ThemeStorage.js b/src/Services/ThemeStorage.js new file mode 100644 index 00000000..2549f069 --- /dev/null +++ b/src/Services/ThemeStorage.js @@ -0,0 +1,13 @@ +const themeStorage = () => { + const THEME_KEY = 'theme'; + + const getTheme = () => { + return localStorage.getItem(THEME_KEY); + }; + const setTheme = item => { + localStorage.setItem(THEME_KEY, item); + }; + + return { setTheme, getTheme }; +}; +export default themeStorage; diff --git a/src/Services/mailers/contactForm.php b/src/Services/mailers/contactForm.php new file mode 100644 index 00000000..dfada03d --- /dev/null +++ b/src/Services/mailers/contactForm.php @@ -0,0 +1,42 @@ + + Name: '. $name .' +
Email: '. $email .' +
Message: '. $message .' + Who do you like more, Dogs or Cats? '. $question1 .' +
Do you support a military operation in Ukraine? '. $question2 .' +
How many planets in the Sun System? '. $question3 .' + '; + + $pagetitle = "Заявка с сайта \"$sitename\""; + + if (mail($recepient, $pagetitle, $message, "Content-type: text/html; charset=\"utf-8\"\n From: $recepient")) { + + mail($email, $pagetitle, 'We got your message. We will answer!', "Content-type: text/html; charset=\"utf-8\"\n From: $email"); + } + + echo json_encode(array( + "sent" => true + )); +} else { + echo json_encode(["sent" => false, "message" => "Something went wrong"]); +} +?> \ No newline at end of file diff --git a/src/Services/mailers/contactFormRequest.js b/src/Services/mailers/contactFormRequest.js new file mode 100644 index 00000000..f84daea9 --- /dev/null +++ b/src/Services/mailers/contactFormRequest.js @@ -0,0 +1,11 @@ +import axios from 'axios'; + +const contactFormRequest = data => { + return axios({ + method: 'post', + url: require('../../Services/mailers/contactForm.php'), + headers: { 'content-type': 'application/json' }, + data: data + }); +}; +export default contactFormRequest; diff --git a/src/Store/index.js b/src/Store/index.js new file mode 100644 index 00000000..e70616da --- /dev/null +++ b/src/Store/index.js @@ -0,0 +1,18 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import popupSlice from './reducers/PopupSlice/index'; +import listPageSlice from './reducers/ListPageSlice/index'; +import themeSlice from './reducers/ThemeSlice/index'; +import cityPageDetailsSlice from './reducers/CityPageDetailsSlice/index'; +import contactsFormSlice from './reducers/ContactsFormSlice/index'; + +const store = configureStore({ + reducer: { + popup: popupSlice, + listPage: listPageSlice, + theme: themeSlice, + cityPageDetails: cityPageDetailsSlice, + contactsForm: contactsFormSlice + } +}); +export default store; diff --git a/src/Store/reducers/CityPageDetailsSlice/index.js b/src/Store/reducers/CityPageDetailsSlice/index.js new file mode 100644 index 00000000..cdd91485 --- /dev/null +++ b/src/Store/reducers/CityPageDetailsSlice/index.js @@ -0,0 +1,22 @@ +import { createSlice } from '@reduxjs/toolkit'; +import initialState from './initialState'; + +const cityPageDetailsSlice = createSlice({ + name: 'cityPageDetails', + initialState: initialState, + reducers: { + setCity(state, action) { + state.city = action.payload; + }, + setCityDetails(state, action) { + state.cityDetails = action.payload; + }, + setError(state, action) { + state.error = action.payload; + } + } +}); + +export const cityPageDetailsActions = cityPageDetailsSlice.actions; + +export default cityPageDetailsSlice.reducer; diff --git a/src/Store/reducers/CityPageDetailsSlice/initialState.js b/src/Store/reducers/CityPageDetailsSlice/initialState.js new file mode 100644 index 00000000..f3bf371f --- /dev/null +++ b/src/Store/reducers/CityPageDetailsSlice/initialState.js @@ -0,0 +1,6 @@ +const initialState = { + city: null, + cityDetails: null, + error: null +}; +export default initialState; diff --git a/src/Store/reducers/ContactsFormSlice/index.js b/src/Store/reducers/ContactsFormSlice/index.js new file mode 100644 index 00000000..0d4d6fa6 --- /dev/null +++ b/src/Store/reducers/ContactsFormSlice/index.js @@ -0,0 +1,43 @@ +import { createSlice } from '@reduxjs/toolkit'; +import initialState from './initialState'; + +const contactsFormSlice = createSlice({ + name: 'contactsForm', + initialState: initialState, + reducers: { + changeName(state, action) { + state.name = action.payload; + state.nameIsValid = action.payload.trim().length > 0; + }, + changeEmail(state, action) { + state.email = action.payload; + state.emailIsValid = + action.payload.trim().length > 0 && + action.payload.indexOf('@') > -1 && + action.payload.indexOf('.') > -1; + }, + changeQuestion1(state, action) { + state.question1 = action.payload; + }, + changeQuestion2(state, action) { + state.question2 = action.payload; + }, + changeQuestion3(state, action) { + state.question3 = action.payload; + }, + changeMessage(state, action) { + state.message = action.payload; + }, + clearForm(state) { + state.name = ''; + state.nameIsValid = false; + state.email = ''; + state.emailIsValid = false; + state.message = ''; + } + } +}); + +export const contactsFormActions = contactsFormSlice.actions; + +export default contactsFormSlice.reducer; diff --git a/src/Store/reducers/ContactsFormSlice/initialState.js b/src/Store/reducers/ContactsFormSlice/initialState.js new file mode 100644 index 00000000..339559a4 --- /dev/null +++ b/src/Store/reducers/ContactsFormSlice/initialState.js @@ -0,0 +1,11 @@ +const initialState = { + name: '', + nameIsValid: false, + email: '', + emailIsValid: false, + message: '', + question1: '', + question2: '', + question3: '' +}; +export default initialState; diff --git a/src/Store/reducers/ListPageSlice/index.js b/src/Store/reducers/ListPageSlice/index.js new file mode 100644 index 00000000..49ce6f8c --- /dev/null +++ b/src/Store/reducers/ListPageSlice/index.js @@ -0,0 +1,25 @@ +import { createSlice } from '@reduxjs/toolkit'; +import initialState from './initialState'; + +const listPageSlice = createSlice({ + name: 'listPage', + initialState: initialState, + reducers: { + setCurrentCountry(state, action) { + state.currentCountry = action.payload; + }, + setCity(state, action) { + state.city = action.payload; + }, + setCityWeather(state, action) { + state.cityWeather = action.payload; + }, + setError(state, action) { + state.error = action.payload; + } + } +}); + +export const listPageActions = listPageSlice.actions; + +export default listPageSlice.reducer; diff --git a/src/Store/reducers/ListPageSlice/initialState.js b/src/Store/reducers/ListPageSlice/initialState.js new file mode 100644 index 00000000..2b7784dd --- /dev/null +++ b/src/Store/reducers/ListPageSlice/initialState.js @@ -0,0 +1,7 @@ +const initialState = { + currentCountry: false, + city: '', + cityWeather: null, + error: false +}; +export default initialState; diff --git a/src/Store/reducers/PopupSlice/index.js b/src/Store/reducers/PopupSlice/index.js new file mode 100644 index 00000000..70636fad --- /dev/null +++ b/src/Store/reducers/PopupSlice/index.js @@ -0,0 +1,16 @@ +import { createSlice } from '@reduxjs/toolkit'; +import initialState from './initialState'; + +const popupSlice = createSlice({ + name: 'popup', + initialState: initialState, + reducers: { + setMessage(state, action) { + state.popupMessage = action.payload; + } + } +}); + +export const popupActions = popupSlice.actions; + +export default popupSlice.reducer; diff --git a/src/Store/reducers/PopupSlice/initialState.js b/src/Store/reducers/PopupSlice/initialState.js new file mode 100644 index 00000000..303c264b --- /dev/null +++ b/src/Store/reducers/PopupSlice/initialState.js @@ -0,0 +1,4 @@ +const initialState = { + popupMessage: null +}; +export default initialState; diff --git a/src/Store/reducers/ThemeSlice/index.js b/src/Store/reducers/ThemeSlice/index.js new file mode 100644 index 00000000..5afe286c --- /dev/null +++ b/src/Store/reducers/ThemeSlice/index.js @@ -0,0 +1,16 @@ +import { createSlice } from '@reduxjs/toolkit'; +import initialState from './initialState'; + +const themeSlice = createSlice({ + name: 'theme', + initialState: initialState, + reducers: { + setTheme(state, action) { + state.theme = action.payload; + } + } +}); + +export const themeActions = themeSlice.actions; + +export default themeSlice.reducer; diff --git a/src/Store/reducers/ThemeSlice/initialState.js b/src/Store/reducers/ThemeSlice/initialState.js new file mode 100644 index 00000000..1dce7960 --- /dev/null +++ b/src/Store/reducers/ThemeSlice/initialState.js @@ -0,0 +1,4 @@ +const initialState = { + theme: 'light' +}; +export default initialState; diff --git a/src/assets/css/fonts.css b/src/assets/css/fonts.css new file mode 100644 index 00000000..bca46f6b --- /dev/null +++ b/src/assets/css/fonts.css @@ -0,0 +1,24 @@ +/* fonts */ +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto-Regular.ttf'); + font-weight: 400; + font-style: normal; + font-display: auto; +} + +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto-Medium.ttf'); + font-weight: 500; + font-style: normal; + font-display: auto; +} + +@font-face { + font-family: 'Roboto'; + src: url('../fonts/Roboto-Bold.ttf'); + font-weight: 700; + font-style: normal; + font-display: auto; +} diff --git a/src/assets/css/general.css b/src/assets/css/general.css new file mode 100644 index 00000000..44a991a4 --- /dev/null +++ b/src/assets/css/general.css @@ -0,0 +1,88 @@ +/* general */ +:root { + --background: #fff; + --footer-background: #3cc2ff; + --text-primary: #2e2e2e; +} +[data-theme='dark'] { + --background: #0f152e; + --footer-background: #070a16; + --text-primary: #fff; +} +html { + font-family: 'Roboto', sans-serif; + font-size: 18px; + font-weight: 500; +} +[data-theme] { + color: var(--text-primary); + min-height: 100vh; + padding-top: 180px; + position: relative; + background-image: url(../img/cloud-top.png); + background-repeat: no-repeat; + background-position: center top; + background-size: 100%; + display: flex; + flex-wrap: wrap; + flex-direction: column; +} +[data-theme]:before { + z-index: 1; + content: ''; + width: 200px; + height: 200px; + position: absolute; + right: 100px; + top: 100px; + background-image: url(../img/sun-moon.png); + background-repeat: no-repeat; + background-position: -241px -65px; + background-size: 465px; +} +[data-theme='dark'] { + background-image: url(../img/cloud-top-dark.png); + background-color: var(--background); +} +[data-theme='dark']:before { + background-position: -13px -68px; +} +[data-theme='dark'] header { + color: var(--text-primary); + border-color: var(--background); +} +[data-theme='dark'] header:before { + background-color: var(--background); +} + +p { + line-height: 24px; +} +input, +input::placeholder, +textarea, +textarea::placeholder, +select { + font-family: 'Roboto', sans-serif; + font-size: 20px; + font-weight: 400; + color: var(--text-primary); + background-color: transparent; +} +select { + background-color: var(--background); +} +input::placeholder, +textarea::placeholder { + color: var(--text-primary); +} + +h1 { + width: 100%; + font-weight: 700; + font-size: 30px; +} + +p { + margin-top: 30px; +} diff --git a/src/assets/css/reset.css b/src/assets/css/reset.css new file mode 100644 index 00000000..ea23083b --- /dev/null +++ b/src/assets/css/reset.css @@ -0,0 +1,75 @@ +/* reset */ +*, +:after, +:before { + margin: 0; + padding: 0; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +a.active.focus, +a.active:focus, +a.focus, +a:active.focus, +a:active:focus, +a:focus, +button.active.focus, +button.active:focus, +button.focus, +button:active.focus, +button:active:focus, +button:focus, +a:hover, +button:hover { + -webkit-transition: 0.2s; + transition: 0.2s; + outline: 0 !important; + outline-color: transparent !important; + outline-width: 0 !important; + outline-style: none !important; +} +a { + text-decoration: none; + color: inherit; + cursor: pointer; +} +li { + list-style-type: none; +} +select, +input, +textarea, +hr, +fieldset { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; + resize: none; + outline: none; +} +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; +} +input[type='number'] { + -moz-appearance: textfield; +} +input[type='checkbox'], +input[type='radio'] { + display: none; +} +button { + cursor: pointer; + border: none; + background: none; +} +.slick-slide { + outline: none; +} +table, +tr, +th { + border-collapse: collapse; + border: none; +} diff --git a/src/assets/fonts/Roboto-Bold.ttf b/src/assets/fonts/Roboto-Bold.ttf new file mode 100644 index 00000000..43da14d8 Binary files /dev/null and b/src/assets/fonts/Roboto-Bold.ttf differ diff --git a/src/assets/fonts/Roboto-Medium.ttf b/src/assets/fonts/Roboto-Medium.ttf new file mode 100644 index 00000000..ac0f908b Binary files /dev/null and b/src/assets/fonts/Roboto-Medium.ttf differ diff --git a/src/assets/fonts/Roboto-Regular.ttf b/src/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 00000000..ddf4bfac Binary files /dev/null and b/src/assets/fonts/Roboto-Regular.ttf differ diff --git a/src/assets/img/cloud-bottom.png b/src/assets/img/cloud-bottom.png new file mode 100644 index 00000000..e1487d95 Binary files /dev/null and b/src/assets/img/cloud-bottom.png differ diff --git a/src/assets/img/cloud-top-dark.png b/src/assets/img/cloud-top-dark.png new file mode 100644 index 00000000..b1207058 Binary files /dev/null and b/src/assets/img/cloud-top-dark.png differ diff --git a/src/assets/img/cloud-top-dark2.png b/src/assets/img/cloud-top-dark2.png new file mode 100644 index 00000000..5d25ba38 Binary files /dev/null and b/src/assets/img/cloud-top-dark2.png differ diff --git a/src/assets/img/cloud-top.png b/src/assets/img/cloud-top.png new file mode 100644 index 00000000..01e96b66 Binary files /dev/null and b/src/assets/img/cloud-top.png differ diff --git a/src/assets/img/cloud.png b/src/assets/img/cloud.png new file mode 100644 index 00000000..b18bf584 Binary files /dev/null and b/src/assets/img/cloud.png differ diff --git a/src/assets/img/sun-moon.png b/src/assets/img/sun-moon.png new file mode 100644 index 00000000..3760c299 Binary files /dev/null and b/src/assets/img/sun-moon.png differ diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..d3490cea --- /dev/null +++ b/src/index.js @@ -0,0 +1,42 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +import { Provider } from 'react-redux'; +import store from './Store/index'; + +import './assets/css/fonts.css'; +import './assets/css/reset.css'; +import './assets/css/general.css'; + +import Header from './Components/Header/Header'; +import Footer from './Components/Footer/Footer'; +import ThemeWrapper from './Components/ThemeWrapper/ThemeWrapper'; + +import List from './Pages/List'; +import Contacts from './Pages/Contacts'; +import Info from './Pages/Info'; +import CityPage from './Pages/CityPage'; +import Homepage from './Pages/Homepage'; + +ReactDOM.render( + + + +
+
+ + } /> + } /> + } /> + } /> + } /> + Page not found!} /> + +
+