diff --git a/README.md b/README.md index 4b7fdb3..8e10149 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,13 @@ -# React Bootstrap +# React Weather App :umbrella: -React boilerplate project for Manchester Codes' projects. +Get the weather forecast :boom: app created with React Component Framework. -## Getting Started +![Screenshot from 2019-04-23 20-03-53](https://user-images.githubusercontent.com/26323783/56609685-9c05b480-6605-11e9-93d3-7ceccde52715.png) -### Clone down this repository (replace ``: - -```bash -git clone git@github.com:MCRcodes/react-bootstrap.git -``` - -### Install dependencies - -```bash -npm install -``` - -### Start up the application: +## to start the app... ```bash npm start ``` ### Visit `localhost:8080` in your browser. - -You should see a **Hello World** message. - -### Change the rendered output - -You can change what is mounted to the DOM in `src/index.jsx`. - -It might be a good idea to make an `App` component inside `App.jsx` (will likely handle your layout and routing), and to mount this to the DOM. diff --git a/__jest__/stub.js b/__jest__/stub.js index e69de29..0a69e0d 100644 --- a/__jest__/stub.js +++ b/__jest__/stub.js @@ -0,0 +1,3 @@ +const stub = {}; + +export default stub; diff --git a/__tests__/components/forecast-summaries.test.jsx b/__tests__/components/forecast-summaries.test.jsx new file mode 100644 index 0000000..1c3ffa9 --- /dev/null +++ b/__tests__/components/forecast-summaries.test.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import ForecastSummary from '../../components/forcast-summary'; +import ForecastSummaries from '../../components/forecast-summaries'; +import Enzyme from 'enzyme'; + +const forecasts = [ + { + date: 'date1', + description: 'desc1', + icon: 'icon1', + temperature: { + max: 999 + } + }, + { + date: 'date2', + description: 'desc2', + icon: 'icon2', + temperature: { + max: 909 + } + } +]; +it('renders the correct amount of summary components from the parent', () => { + const wrapper = Enzyme.shallow(); + expect(wrapper.find(ForecastSummary).length).toBe(2); +}); + +it('takes the forecast and passes the corect date values', () => { + const wrapper = Enzyme.shallow(); + expect( + wrapper.find(ForecastSummary).forEach((node, index) => { + expect(node.prop('date')).toEqual(forecasts[index].date); + expect(node.prop('description')).toEqual(forecasts[index].description); + expect(node.prop('icon')).toEqual(forecasts[index].icon); + expect(node.prop('temperature')).toEqual( + forecasts[index].temperature.max + ); + }) + ); +}); diff --git a/__tests__/components/forecast-summary.test.jsx b/__tests__/components/forecast-summary.test.jsx new file mode 100644 index 0000000..f24a3b4 --- /dev/null +++ b/__tests__/components/forecast-summary.test.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import Enzyme from 'enzyme'; +import ForecastSummary from '../../components/forcast-summary'; + +it('takes temperature, date, descripte and icon and displays them in spans', () => { + const wrapper = Enzyme.shallow( + + ); + expect(wrapper.find('.forecast-summary-temperature').text()).toEqual( + 'temperature Max: mockTemp℃' + ); +}); diff --git a/__tests__/components/location-details.test.jsx b/__tests__/components/location-details.test.jsx new file mode 100644 index 0000000..8f8405c --- /dev/null +++ b/__tests__/components/location-details.test.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import Enzyme from "enzyme"; +import LocationDetails from "../../components/location-details"; + +it("takes a city and country and renders inside a h1", () => { + const wrapper = Enzyme.shallow(); + + const text = wrapper.find("h1").text(); + expect(text).toBe("foo, bar"); +}); diff --git a/components/app.jsx b/components/app.jsx new file mode 100644 index 0000000..090b395 --- /dev/null +++ b/components/app.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import LocationDetails from './location-details'; +import ForecastSummaries from './forecast-summaries'; +import ForecastDetails from './forecast-detils'; +import SearchForm from './search-form'; + +import '../src/styles/app.scss'; + +const URL = 'https://mcr-codes-weather.herokuapp.com/forecast?city='; + +class App extends React.Component { + constructor(props) { + super(props); + this.state = { + selectedDate: 0, + forecasts: [], + location: { + city: 'manchester', + country: '', + }, + }; + } + + goFetchNow = city => { + fetch(`${URL}${city}`) + .then(data => data.json()) + .then(data => { + this.setState({ + forecasts: data.forecasts, + location: { + city: data.location.city, + country: data.location.country, + }, + }); + }); + }; + + componentDidMount() { + this.goFetchNow(this.state.location.city); + } + + handleForecastSelected = date => { + this.setState({ + selectedDate: date, + }); + }; + + render() { + const chooseSelectedForecastByDate = this.state.forecasts.find(forecast => { + return forecast.date === this.state.selectedDate.date; + }); + + return ( +
+ + +
+ +
+ {chooseSelectedForecastByDate && ( + + )} +
+ ); + } +} + +export default App; diff --git a/components/forcast-summary.jsx b/components/forcast-summary.jsx new file mode 100644 index 0000000..2cfe336 --- /dev/null +++ b/components/forcast-summary.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import WeatherIcon from 'react-icons-weather'; +import moment from 'moment'; + +const dateStyle = { + color: '#f5d6ff', +}; +const iconStyle = { + display: 'flex', + justifyContent: 'center', + position: 'relative', + fontSize: '80px', + color: '#bcf2ef', +}; + +const btnStyle = { + display: 'flex', + justifyContent: 'center', + position: 'relative', +}; + +const ForecastSummary = props => { + return ( +
+ {moment(props.date).format('ddd-Do-MMM')} +
+ {props.description} +
+
+ +
+ + temperature Max: {props.temperature}℃ + +
+ +
+ ); +}; + +export default ForecastSummary; diff --git a/components/forecast-detils.jsx b/components/forecast-detils.jsx new file mode 100644 index 0000000..89dd1ef --- /dev/null +++ b/components/forecast-detils.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import moment from 'moment'; + +const detailStyle = { + border: '3px black solid', + borderRadius: '7px', + padding: '10px 10px', + marginBottom: '7px', +}; + +const forecastDetailsWrapper = { + display: 'flex', + justifyContent: 'center', +}; + +const ForecastDetails = props => { + const { date, wind, humidity, temperature } = props.detail; + return ( +
+
+ date: {moment(date).format('Do-MMM-YY ')} +
+ + temperature min: {temperature.min}°C max: {temperature.max}°C + +
+ humidity: {humidity}% +
+ wind speed: {wind.speed} mph +
+ wind direction: {wind.direction.toUpperCase()} +
+
+ ); +}; + +export default ForecastDetails; diff --git a/components/forecast-summaries.jsx b/components/forecast-summaries.jsx new file mode 100644 index 0000000..4fadfee --- /dev/null +++ b/components/forecast-summaries.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ForecastSummary from './forcast-summary'; +import '../src/styles/forecast-summaries.scss'; + +const ForecastSummaries = props => { + return ( +
+ {props.forecasts.map(forecast => ( + + ))} +
+ ); +}; + +ForecastSummaries.propTypes = { + onSelect: PropTypes.func, +}; + +export default ForecastSummaries; diff --git a/components/location-details.jsx b/components/location-details.jsx new file mode 100644 index 0000000..826376a --- /dev/null +++ b/components/location-details.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const LocationDetails = props => ( +

+ {props.city}, {props.country} +

+); + +LocationDetails.propTypes = { + city: PropTypes.string.isRequired, + country: PropTypes.string.isRequired, +}; + +export default LocationDetails; diff --git a/components/search-form.jsx b/components/search-form.jsx new file mode 100644 index 0000000..a4546ba --- /dev/null +++ b/components/search-form.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +class SearchForm extends React.Component { + constructor(props) { + super(props); + this.state = { + searchText: '', + }; + } + + handleSearch = e => { + e.preventDefault(); + this.props.cityCallback(this.state.searchText); + this.setState({ + searchText: '', + }); + }; + + handlechange = e => this.setState({ searchText: e.target.value }); + + render() { + return ( +
+ + +
+ ); + } +} + +export default SearchForm; diff --git a/index.html b/index.html index 5c7f083..1814a9a 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,16 @@ - - - React App - - - -
- - + + + + React App + + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json index bd6d23b..714933e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4667,7 +4667,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4688,12 +4689,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4708,17 +4711,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4835,7 +4841,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4847,6 +4854,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4861,6 +4869,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4868,12 +4877,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4892,6 +4903,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4972,7 +4984,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4984,6 +4997,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5069,7 +5083,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5105,6 +5120,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5124,6 +5140,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5167,12 +5184,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -8117,6 +8136,11 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "moo": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", @@ -9968,6 +9992,11 @@ "schedule": "^0.5.0" } }, + "react-icons-weather": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-icons-weather/-/react-icons-weather-1.0.5.tgz", + "integrity": "sha512-FdFL3Pbtby2kg0/40FkUNDyn5hyFGqwRH8/IGAflr9L7m76DPnW5E7c+V4dGpvqoAR1ua7AlEt2MeKxSYESasg==" + }, "react-is": { "version": "16.5.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.5.2.tgz", diff --git a/package.json b/package.json index 40c9b20..4e419f2 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "start": "webpack-dev-server", "test": "jest" }, + "moduleNameMapper": { + "^.+\\.s?css$": "/__jest__/stub.js" + }, "keywords": [ "react" ], @@ -21,10 +24,12 @@ }, "homepage": "https://github.com/MCRcodes/react-bootstrap#readme", "dependencies": { + "moment": "^2.24.0", "prop-types": "^15.6.1", "raf": "^3.4.0", "react": "^16.3.2", - "react-dom": "^16.3.2" + "react-dom": "^16.3.2", + "react-icons-weather": "^1.0.5" }, "devDependencies": { "babel-jest": "^22.4.3", diff --git a/src/data/forecast.json b/src/data/forecast.json new file mode 100644 index 0000000..be8a109 --- /dev/null +++ b/src/data/forecast.json @@ -0,0 +1,77 @@ +{ + "location": { + "city": "Manchester", + "country": "UK" + }, + "forecasts": [{ + "date": 1525046400000, + "temperature": { + "max": 11, + "min": 4 + }, + "wind": { + "speed": 13, + "direction": "s" + }, + "humidity": 30, + "description": "Clear", + "icon": "800" + }, + { + "date": 1525132800000, + "temperature": { + "max": 13, + "min": 8 + }, + "wind": { + "speed": 60, + "direction": "ne" + }, + "humidity": 80, + "description": "Stormy", + "icon": "211" + }, + { + "date": 1525219200000, + "temperature": { + "max": 1, + "min": -2 + }, + "wind": { + "speed": 5, + "direction": "n" + }, + "humidity": 50, + "description": "Heavy Snow", + "icon": "602" + }, + { + "date": 1525305600000, + "temperature": { + "max": 20, + "min": 4 + }, + "wind": { + "speed": 150, + "direction": "e" + }, + "humidity": 80, + "description": "Tornado", + "icon": "781" + }, + { + "date": 1525392000000, + "temperature": { + "max": 25, + "min": 18 + }, + "wind": { + "speed": 8, + "direction": "nne" + }, + "humidity": 50, + "description": "Hazy", + "icon": "721" + } + ] +} diff --git a/src/index.jsx b/src/index.jsx index ba32e3b..967ac12 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,5 +1,6 @@ import 'raf/polyfill'; import React from 'react'; import { render } from 'react-dom'; +import App from '../components/app'; -render(
Hello World!
, document.getElementById('root')); +render(, document.getElementById('root')); diff --git a/src/styles/app.scss b/src/styles/app.scss new file mode 100644 index 0000000..5edc90f --- /dev/null +++ b/src/styles/app.scss @@ -0,0 +1,16 @@ +.forecast { + background-color: aquamarine; + margin-left: 20px; + margin-right: 20px; + font-family: 'Rubik', sans-serif; + font-weight: 900; + font-size: 20px; +} + +.forecast-summary { + background-color: darkmagenta; + border-radius: 3px; + padding: 10px 5px; + color: orangered; + margin: 5px 5px; +} diff --git a/src/styles/forecast-summaries.scss b/src/styles/forecast-summaries.scss new file mode 100644 index 0000000..6bb6162 --- /dev/null +++ b/src/styles/forecast-summaries.scss @@ -0,0 +1,4 @@ +.forecast-summaries { + display: flex; + justify-content: space-between; +}