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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

/app/store/testdata.js

# dependencies
/node_modules
/.pnp
Expand Down
11 changes: 11 additions & 0 deletions app/components/NewSparkline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Sparklines, SparklinesLine, SparklinesReferenceLine, SparklinesSpots } from 'react-sparklines';

export function NewSparkline({ data, color = 'grey', lineType = 'avg' }) {
return (
<Sparklines data={data} width={100} height={30}>
<SparklinesLine style={{stroke: color, strokeWidth: ".75", fill: color, fillOpacity: ".25"}}/>
<SparklinesReferenceLine type={lineType}/>
{/* <SparklinesSpots size={.75} style={{stroke: color, fill: color}}/> */}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove commented code

</Sparklines>
);
};
65 changes: 65 additions & 0 deletions app/components/WeatherSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { fetchForecast } from '../store/slices/cities';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

export function WeatherSearch() {
const schema = yup.object({ // using yup to help build a schema for react-hook-form
search: yup
.string()
.required('A city name is required to search.')
.test('already-searched', 'This city has already been searched.', (value) => {
return !searches.includes(value?.toLowerCase());
})
});
const dispatch = useDispatch();
const [search, setSearch] = useState(''); // Keeps track of the current search
const [searches, setSearches] = useState([]); // Keeps track of previous search values

const { // using react-hook-form for form validation
register,
handleSubmit,
formState: { errors },
} = useForm({ resolver: yupResolver(schema), });

const handleFormSubmit = async () => {
setSearches([...searches, search.toLowerCase()]); // Adds search to previous searches

try {
dispatch(fetchForecast(search)); // Dispatch asyncThunk in cities slice
} catch (error) {
console.error(error);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't help in terms of UX, alert or showing errors is better

}
setSearch(''); // Clear the input field after each search
};

return (
<div>
<form onSubmit={handleSubmit(handleFormSubmit)} className='d-flex justify-content-center'>
<div className='form-group col-8 d-flex'>
<input
{...register('search')}
value={search}
className='form-control'
placeholder='Get a five-day forecast in your favorite cities'
onChange={event => setSearch(event.target.value)}
/>
<button
className='btn btn-secondary'
type='submit'
>
Submit
</button>
</div>
</form>
<br/>
<div className='d-flex justify-content-center'>
{errors.search?.message && (
<div className='text-danger'>{errors.search?.message}</div>
)}
</div>
</div>
);
};
32 changes: 32 additions & 0 deletions app/components/WeatherTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client'
import { WeatherTableRow } from './WeatherTableRow';
import { useSelector } from 'react-redux';

/**
* This compenent renders a table for weather data.
* Includes columns for city name, temp, pressure, and humidity.
* @returns {ReactNode} A React element that renders a table to display the weather.
*/
export function WeatherTable() {
const cities = useSelector((state) => state.cities.cities);

return (
<div className='container'>
<div className='col-12 text-center'>
<table className='table'>
<thead className='table-dark'>
<tr>
<th>City</th>
<th>Temperature (F)</th>
<th>Pressure (hPa)</th>
<th>Humidity (%)</th>
</tr>
</thead>
<tbody>
{cities.map(cityObj => (<WeatherTableRow city={cityObj} key={cityObj.city.id}/>))}
</tbody>
</table>
</div>
</div>
)
}
54 changes: 54 additions & 0 deletions app/components/WeatherTableRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { getItemsPerDay, getAvgItemsPerDay, avg, convertKToF, getHighsAndLows } from './functions';
import { NewSparkline } from './NewSparkline';

/**
* This component renders a table row with the specific weather data for a searched city.
* @param {Object} city
* @returns {ReactElement}
*/
export function WeatherTableRow({ city }) {
/** Gets an array of only the temp data */
const temps = city.list.map(weather => weather.main.temp);
/** Groups temps by day */
const allTempsPerDay = getItemsPerDay(temps);
/** Gets the highs and lows per day and converts them to Fahrenheit*/
const highAndLowTemps = getHighsAndLows(allTempsPerDay).map(day => convertKToF(day));
/** Gets the average temp per day and converts them to Fahrenheit*/
const avgTempsPerDay = getAvgItemsPerDay(allTempsPerDay).map(day => convertKToF(day));

/** Gets an array of only pressure data */
const pressures = city.list.map(weather => weather.main.pressure);
/** Groups pressures by day */
const allPressuresPerDay = getItemsPerDay(pressures);
/** Gets the high and low pressures for each day */
const highAndLowPressures = getHighsAndLows(allPressuresPerDay);
/** Gets the average pressure per day */
const avgPressuresPerDay = getAvgItemsPerDay(allPressuresPerDay)

/** Gets an array of only humidity data */
const humidities = city.list.map(weather => weather.main.humidity);
/** Groups humidity by day */
const allHumiditiesPerDay = getItemsPerDay(humidities);
/** Gets the high and low humidity for each day */
const highAndLowHumidities = getHighsAndLows(allHumiditiesPerDay);
/** Gets the average humidity per day */
const avgHumiditiesPerDay = getAvgItemsPerDay(allHumiditiesPerDay);

return (
<tr>
<td>{city.city.name}, {city.city.country}</td>
<td>
<NewSparkline data={highAndLowTemps} color='orange'/>
<div>Avg: {avg(avgTempsPerDay)} F</div>
</td>
<td>
<NewSparkline data={highAndLowPressures} color='green'/>
<div>Avg: {avg(avgPressuresPerDay)} hPa</div>
</td>
<td>
<NewSparkline data={highAndLowHumidities} color='blue'/>
<div>Avg: {avg(avgHumiditiesPerDay)} %</div>
Comment on lines +42 to +50

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are doing all the average, why here calling avg again?

</td>
</tr>
);
}
71 changes: 71 additions & 0 deletions app/components/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Takes an array of 40 data points and groups them into 5 sub-arrays
* Each sub-array represents a day.
* @param {Array} arr
* @returns {Array}
*/
export const getItemsPerDay = (arr) => {
const chunk = 8;

const groups = arr.reduce((result, item, index) => {
if (index % chunk === 0) {
result.push(arr.slice(index, index + chunk));
}
return result;
}, []);
return groups;
};

/**
* Iterates through the data points grouped by day and returns the average of each group
* @param {Array} arr
* @returns {Array}
*/
export const getAvgItemsPerDay = (arr) => {
return arr.reduce((result, item) => {
result.push(avg(item));
return result;
}, []);
};

/**
* Returns an average of a supplied array of numbers
* @param {Array} arr
* @returns {Number}
*/
export const avg = (arr) => {
return Math.round(arr.reduce((result, item) => result + item, 0 ) / arr.length);
};

/**
* Assuming that the passed value is in Kelvin, this function will convert it to Fahrenheit.
* @param {Number} temp
* @returns {Number}
*/
export const convertKToF = (temp) => {
return Math.round(1.8 * (temp - 273) + 32);
};

/**
* Iterates through the data points grouped by day and returns the high and low for each day
* @param {Array} arr
* @returns {Array}
*/
export const getHighsAndLows = (arr) => {
const newArr = []

arr.forEach(subarr => {

const high = subarr.reduce((acc, cur) => {
return acc > cur ? acc : cur;
})

const low = subarr.reduce((acc, cur) => {
return acc <= cur ? acc : cur;
})

newArr.push(high, low);
})

return newArr;
};
Binary file removed app/favicon.ico
Binary file not shown.
107 changes: 0 additions & 107 deletions app/globals.css

This file was deleted.

19 changes: 8 additions & 11 deletions app/layout.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import './globals.css'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
'use client'
import 'bootstrap/dist/css/bootstrap.min.css';
import { Provider } from 'react-redux';
import store from './store/configureStore';

export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body>
<Provider store={store}>{children}</Provider>
</body>
</html>
)
);
}
Loading