Skip to content

williampansky/react-weather-app-exercise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Weather App

Simple weather app using create-react-app in CodeSandbox.

Assignment

Develop a weather application with the following specifications:

  • It should show the 5-day weather forecast for Dallas, TX.
  • Your application should utilize ReactJS (usage of boilerplates such as Create React App are encouraged).
  • You should be able to toggle between Fahrenheit and Celsius.
  • It should match the provided comp as closely as possible on desktop.
  • We do not provide a mobile mockup as you will determine how that functions on your own.
  • Icons should correspond to proper weather conditions.
  • It should be responsive, rendering appropriately on tablet, and handheld form factors.
  • Your application may use any open weather API.
  • Your application should showcase one animation technique of your choosing in order to give the application some life.
  • (Optional) Allow for user input to change location of forecast (city, state, or zip)

Documentation & reference

Todo

  • Initial research.
  • Setup base-level project scaffold.
  • Plan out components.
    • AppDay.jsx - Displays a single day of the upcoming 5-day week. It visually shows a user an abbreviated day name, an icon representing the day's projected weather conditions, and the temperature from the api call.
    • AppGraphic.jsx - Displays a static image representing the city/location selected; Dallas in this example.
    • ...
  • Develop base-level app wireframe.
  • Install styled-components package.
  • Fine-tune scaffold and components.
  • Polish stylesheets.
  • Download assets from Zelplin.io provided comp.
  • Integrate weather API.
  • Integrate accessibility.
  • Integrate Schema/SEO.
  • Integrate webapp (SPA) icons into HTML file.
  • Integrate meta tags into HTML file.
  • Refactor for responsiveness.
  • Refactor for semantic HTML.
  • Enhance with animations.
  • Write JEST tests.
  • Test on mobile & tablet devices.
  • Test for WCAG & accessibility.
  • Browser checks.

Difficulties

Api integration & state management

Coming off the heels of a data/api-driven Vue app, a serious hurdle I found myself struggling to get over was how to work with an Api and handle central state management in React. Vuex, Vue's solution to a store, made mutating central state a breeze.

Api response

Reading the Api response and selecting the correct index from the next weekday was initially confusing. Weatherbit's Api for a 5-day forecast returns an array (response.data.data) length of 39. Initially, when passing data into my AppDay component props, I went with the following:

// omitted...
<Week>
  <AppDay day={api.data[1].timestamp_utc} />
  <AppDay day={api.data[2].timestamp_utc} />
  <AppDay day={api.data[3].timestamp_utc} />
  <AppDay day={api.data[4].timestamp_utc} />
  <AppDay day={api.data[5].timestamp_utc} />
</Week>
// etc...

My thought was, "Okay. Today is obviously going to be api.data[0], so the next likely sequence would be [1], [2], [3], and so on. It didn't initially occur to me why a 5-day forecast's data child array would contain 39 entries (haha, silly me...). The length comes due to the endpoint returning 3-hour intervals for daily weather. So, depending on the time you ping it'll return a different response length. So, to resolve this, I opted for using the same service's 16-day/daily forecast endpoint. Now each entry is a single day, therefore my Week can be structed like I had originally anticipated.

Despite all this, I eventually refactored my <AppDay /> component calls into a .map loop—which presents a cleaner source code:

const data = this.state.data;
const week = _.get(data, "data", []).slice(1);

<Week>
  {week.map((data, i) => (
    <AppDay
      key={i}
      day={data.valid_date}
      degrees={data.temp}
      icon={getIcon(data.weather.code)}
      tooltip={data.valid_date}
    />
  ))}
</Week>;

Limiting Api calls

A free plan from weatherbit allows for 1,000 calls/day; therefore I needed a way to limit the number of calls I make. Since CodeSandbox can refresh you project's frontend view on the fly, not setting a limiting function could (and did, haha) send me requests through the roof! I did some Google-foo and came up with the following function:

// omitted...
/**
 * Sets date timestamp tokens to localStorage. Compare token to
 * `threeHoursAgo`. If true, refresh our API.
 * @method refreshApi
 * @see [StackOverflow]{@link https://stackoverflow.com/a/42529483}
 */
const refreshApi = () => {
  const HOUR = 1000 * 60 * 60;
  const THREEHOURS = HOUR * 3;
  const threeHoursAgo = Date.now() - THREEHOURS;

  const token = localStorage.getItem("token");
  if (!token) localStorage.setItem("token", new Date());

  if (token < threeHoursAgo) {
    localStorage.setItem("token", new Date());
    return true;
  } else {
    return false;
  }
};

This little guy simply constructs a 3-hour variable, THREEHOURS, and a variable, threeHoursAgo that subtracts that from Date.now(). It uses threeHoursAgo in conjunction with a localStorage token to return a boolean. If the boolean returns true we'll refresh our Api call:

useEffect(() => {
  if (refreshApi() === true) fetchData();
}, []);

Displaying conditional icons

The app requires that icons, "... correspond to proper weather conditions." Considering there are a possible 38 different codes coming down from the api, and that we currently only have four icons available, a solution was required to ensure an icon displayed on the frontend regardless of the response.data.weather.code value. My solution was a helper function that takes in a number parameter and returns a filename string depending on the group which the param belonged to.

const getIcon = code => {
  const icons = {
    cloudDrizzle: "cloud-drizzle-sun",
    cloudDrizzleSun: "cloud-drizzle-sun",
    cloudLightning: "cloud-lightning",
    cloudSun: "cloud-sun"
  };

  const groups = {
    drizzle: [300, 301, 302],
    general: [800, 801, 802, 803, 804, 900],
    hazards: [700, 711, 721, 731, 741, 751],
    rain: [500, 501, 502, 511, 520, 521, 522],
    snow: [600, 601, 602, 610, 611, 612, 621, 622, 623],
    thunderstorms: [200, 201, 202, 230, 231, 232, 233]
  };

  if (groups.drizzle.includes(code)) return icons.cloudDrizzle;
  else if (groups.general.includes(code)) return icons.cloudSun;
  else if (groups.hazards.includes(code)) return icons.cloudSun;
  else if (groups.rain.includes(code)) return icons.cloudDrizzle;
  else if (groups.snow.includes(code)) return icons.cloudDrizzle;
  else if (groups.thunderstorms.includes(code)) return icons.cloudLightning;
  else return icons.cloudSun;
};

Basically, I defined an icons object that contained key/value pairs of our available icon SVGs. I then constructed a groups object which contained child arrays that represent high-level definitions of weather conditions; such as drizzle or snow.

Now, using a switch statement here would be ideal—however, it could be hacky and abusive of the way case is evaluated (as our conditional relies on array.includes()). Therefore, I crafted an incredibly ugly if/else chain to determine the return output from our function. Some examples this in use are:

getIcon(300); /** @returns {cloud-drizzle-sun} */
getIcon(802); /** @returns {cloud-sun} */
getIcon(511); /** @returns {cloud-drizzle-sun} */
getIcon(233); /** @returns {cloud-lightning} */

Switching between degrees

Handling the functionality to switch between Fahrenheit and Celsius proved particularly difficult.

First, I needed to work with use-persisted-state to retrieve and store both types of JSON strings from the Api.

// before celcius integration
const useWeatherState = createPersistedState("api");
const [api, setData] = useWeatherState();

// after celcius integration
const useWeatherState = createPersistedState("api"),
  useWeatherStateC = createPersistedState("apiC");
const [api, setDataF] = useWeatherState(),
  [apiC, setDataC] = useWeatherStateC();

Next, I needed to find a way to swap between using api and apiC as dynamic parent objects to pass down the data. My first try revolved around setting up a conditional check and a two mutable let declarations:

let api;
const selection = localStorage.getItem("degrees");
if (selection === "F") api = apiF;
else if (selection === "C") api = apiC;
else api = apiF;

let state = {
  cityName: api.city_name,
  stateCode: api.state_code,
  today: {
    temp: api.data[0].temp,
    date: api.data[0].valid_date,
    conditions: api.data[0]
  }
  // etc...
};

This worked on page refresh, however it did not dynamically through a handleDegreesChange(event) function. Therefore, I needed to take a different approach. I recalled the section from the React docs on Lifting State Up, which presents the idea of sharing state, "...by moving it up to the closest common ancestor of the components that need it." This also lit my brain lightbulb to realize I needed to be using setState in this scenario to swap the root data the React way instead of my silly let conditional. You'd think this would all be ovbious—however, I am a novice in React; having spent the entire time of my last enterprise app development in a Vue environment.

So, let's take a look at how I refactored to using setState.

// index.js
import AppRoot from "./App";
function App() {
  // omitted...
  return <AppRoot data={api} dataC={apiC} />;
}

CodeSandbox's create-react-app boilerplate sets up a root function declaration for our project, aptly titled function App(). My project's source-of-truth parent component is imported as AppRoot and then returned with two prop attributes: data (for our base/Fahrenheit api data) and dataC for our metric Celcius api data. So we pass those on down into our App.jsx component:

class App extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            data: '',
            scale: localStorage.getItem('degrees'),
            units: ''
        };
    }
// ...