The Shoppies: My Project Submission for Shopify’s Front-End Internship

gedalyakrycer

Gedalya Krycer

Posted on January 25, 2021

The Shoppies: My Project Submission for Shopify’s Front-End Internship

Page load

The Shoppies Movie Nomination Website

This React website was created as part of the application process for the Shopify Front-End Developer Internship.

I figured it might be helpful for other recent graduates or junior developers to share my process. It is of course not the only way to tackle such a project, but one that I found worked well for me. Hopefully, the folks at Shopify agree. :)

🔗 View Deployed Version

🔗 View GitHub Repository


Table Of Contents


The Challenge

Create a webpage that can search OMDB for movies, and allow the user to save their favorite films they feel should be up for nomination. When they've selected 5 nominees they should be notified they're finished.

Requirements

  • Simple to use interface.
  • The ability to search the OMDB API and return a list of movies that show at least the title, release year, and a button to nominate them.
  • Search results should only be of movies.
  • Updates to the search terms should update the result list.
  • If a movie has been nominated already, it's button should be disabled within in search results.
  • Nominated movies should move to their own "Nomination List".
  • Movies in the nomination list should be able to be removed.
  • Display a banner when the user has 5 nominations.

Provided Reference Image

Reference Image

Extras

Improvements to the design and functionality are allowed, in order to highlight passion and skills.

Back To Top


My Approach

1. Feature list

Alt Text
Using Trello I created a checklist of the requirements and the my additional ideas. This let me keep on top of production against the deadline and quickly capture/prioritize new ideas while I was working.

For this site, I just used a single card, but for larger projects, I would’ve broken up tasks into separate cards on a larger kanban board.

Additional Features

  • Add user-friendly error handling for search
  • Allow users the option to search for series or movies
  • Create new copy (text) that matches the marketing site’a format
  • Create a custom and dynamic responsive layout
  • Add CSS Animations throughout
  • Have a winner be selected at the end
  • Create authentication so people can't view the winner page on their own
  • Make app into a PWA
  • Have the nomination list persist with Local Storage
  • Show expanded information for nominated films, such as ratings
  • Have nominated films link to their IMDB page
  • Use a Loader/Spinner when querying the API for search or nominating
  • Add Open Graph and Twitter assets for sharing to social media

2 — Research

One of the key areas I wanted to play with was the design and keep it within the existing brand family. This way the nomination site would have the trust of the Shopify brand and the winner would carry more impact. (Plus I really love the branding and wanted an opportunity to apply it in a project.)

The first places I checked were the Shopify marketing site and the Polaris design system site. These were valuable to see what sort of layouts, color, and typography were used.

I also took note of the copywriting style for the main service pages and how there was an engaging theme of elements overlaying into other sections, breaking the grid.

Lastly, I attended a webinar hosted by Shopify that had 5 panelists talk about the application process, but more importantly how they approach design and development at Shopify.

Being able to learn the context of projects was really important to them and having a growth mindset. 4 months as an intern goes by fast and they mentioned how important it was to take in as much as possible.

This webinar helped validate for me that applying the context of the exiting brand would be a good direction and that showcasing an ability to adapt to new tech would also work in my favor. (Such as using Redux, which was listed in the job description.)

3 — Creating a new design

Now that I had a feature list and design direction I leveraged Figma for the next stage of planning.

Since I was going to build this application with React, I created a "React Component Flow" that showed what components would be needed and how they would be structured. This also made it easier later on the create the folder structure and quickly brainstorm when a new component was needed.

Next, I created the user interface that was tied to a design system. The design system held styles for typography and colors. It also housed Figma components that were built with Auto Layout and Variants.

Creating these design assets upfront made the coding a lot easier, as many problems could be identified and solved in this initial stage.

🔗 View Figma Designs

4 — Development

By this point, I had everything I needed to start coding and the above resources proved helpful throughout development. I decided on building the site in React and use Redux, SASS, and CSS Animations to support it. These 3 technologies are all areas I can improve in and I wanted this project to be a catalyst for growth, whether it helped to earn the internship or not.

Back To Top


Tech Used

  • React.js (Hooks)
  • Axios
  • OMDB API
  • Dot ENV
  • React Router
  • Redux
  • Redux-Thunk
  • Node SASS
  • CSS Animations
  • CSS Grids
  • Flexbox
  • Figma
  • PWA
  • Local Storage
  • Netlify
  • Trello
  • Git Hub

Back To Top


Code & Feature Highlights

Responsive Layout

Responsive Layout

Layouts are achieved with CSS Grids, Flexbox, Transform and Relative/Absolute positioning. I used SCSS mixins and variables to standardize the media queries across the site and ensure an optimized layout for all screens.

@mixin xxlMinBreakPoint {
    @media (min-width: #{$screen-xxl}) {
        @content;
    }
}

@mixin customMinBreakPoint($size) {
    @media (min-width: $size+'px') {
        @content;
    }
 }
Enter fullscreen mode Exit fullscreen mode

Animation Storage

There are many CSS animations used across the site to introduce and send off different elements.

I created some manually and generated others with animista.net. To help streamline the SCSS files I placed the actual keyframes into an "_animations.scss" partial file.

This allowed animations to be re-used without repeating code and reduced the overall size of the main SCSS files.

components
|— WinnerWrapper.js 
|— winnerWrapper.scss <--- uses animation

scssStyles
|— _animations.scss <--- stores animation
|— _functions.scss
|— _global.scss
|— •••
Enter fullscreen mode Exit fullscreen mode

Debounce

To ensure that multiple API calls are not made with each letter inputted, I used a debounce custom hook to delay the API call until the user finished typing.

 // Sends search term to API
useEffect(() => {

    // Cancels search if nothing is inputted
    if (!searchTerm) {
        return;
    }

    // Send search term to Redux once the Denouncer Hook is ready
    if (debouncedSearchTerm) {
        searchOmdbApi(action.searchOmdb(searchTerm, searchSeries));
    }

}, [debouncedSearchTerm]);
Enter fullscreen mode Exit fullscreen mode

A Spinner/Loader Is Shown During API Calls

This helps the user know something is happening if the API doesn't respond right away.

 // Search Results Display
let searchResults = null;

 // If the person is currently searching...
if (searching) {

    // ...Then a loader will show until the api returns results
    if (searchLoadingStatus || nominationLoadingStatus) {
        searchResults = <Loader />
    } else {

        // Stores the MovieSearchMetaInfo component (which gets mapped through)
        searchResults = movieListArray && movieListArray.map((movie, index) => {

            // Checks if movie has been nominated already
            const isNominated = nominationList.find(result => result.Title === movie.Title)

            return <MovieSearchMetaInfo
                key={movie.imdbID}
                exitResults={triggerExitResults}
                title={movie.Title}
                year={movie.Year}
                type={movie.Type}
                index={index}
                disable={isNominated}
                handleClick={() => handleNominate(movie.Title, movie.Year)}
            />
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Series / Movie Switcher & Series Release Year Fix

Since some people prefer The Office over Star Wars, I felt it was important to also allow people to search series. However, this is separated from the movie search, to follow the project requirements.

First, the Search Action Creator (using redux-thunk) checks to see if the user searching for movies or a series.

// OMDB Movie API
let omdbUrl = null;

// Check to see the user media type preference
if (seriesToggle) {
    omdbUrl = `https://www.omdbapi.com/?s=${searchTerm}&type=series&apikey=${process.env.REACT_APP_OMDB_KEY}`;
} else {
    omdbUrl = `https://www.omdbapi.com/?s=${searchTerm}&type=movie&apikey=${process.env.REACT_APP_OMDB_KEY}`;
}
Enter fullscreen mode Exit fullscreen mode

It then makes the API call. The response is run through several checks, which are described in the below comments.

axios.get(omdbUrl)
    .then((res) => {
        const response = res.data;

        if (response.Response) {
            let resultList = response.Search;

            // Checks if the results list is an array to prevent an error
            if (Array.isArray(resultList)) {

                // Limits the search results to 3 if needed
                resultList = resultList.length > 3 ? resultList.slice(0, 3) : resultList;

                // Series that are still going don't come formatted nicely
                // This loop adds a "Present" to the end if needed
                // Some movies also come formatted incorrectly and they are fixed here
                resultList.forEach(result => {

                    // Creates an array of the year
                    let resultYearArray = result.Year.split('');

                    // If there is no end date this will add a "Present"
                    if (resultYearArray.length < 6
                        && result.Type === "series") {
                        let updatedResultYear = resultYearArray.concat("Present")

                        return result.Year = updatedResultYear.join("")
                    }

                    // If a movie has "-Present", this will remove it
                    if (resultYearArray.length > 4
                        && result.Type === "movie") {
                        let updatedResultYear = resultYearArray.slice(0, 4)

                        return result.Year = updatedResultYear.join("")
                    }
                });
            }

            // Sends the final array to another action creator that talks to the reducer
            dispatch(searchSucceeded(resultList))
        }
Enter fullscreen mode Exit fullscreen mode

Reducer Helper Functions

Helper functions are used within the Reducer stores, to make the switch cases more streamlined.

// Function example that contains some logic
const nominationSuccess = (state, action) => {
    let updatedNominationList = null;

    const movieAlreadyNominated = state.nominationList.find(result => result.Title === action.omdbResult.Title)

    if (movieAlreadyNominated) {
        updatedNominationList = state.nominationList;
    } else {
        updatedNominationList = state.nominationList.concat(action.omdbResult)
    }

    return updateObject(state, {
        loading: false,
        error: false,
        nominationList: updatedNominationList
    });
}

// Greatly streamlined switch case 
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case actionTypes.NOMINATED_STARTED:
            return nominationStarted(state, action);
        case actionTypes.NOMINATED_SUCCESS:
            return nominationSuccess(state, action); // <--- one line used
        case actionTypes.NOMINATED_FAILED:
            return nominationFailed(state, action);
        case actionTypes.NOMINATION_CANCELED:
            return nominationCanceled(state, action);
        case actionTypes.NOMINATIONS_STORED:
            return nominationStored(state, action);
        case actionTypes.NOMINATIONS_COMPLETED:
            return nominationCompleted(state, action);
        case actionTypes.NOMINATIONS_CLEARED:
            return nominationCleared(state, action);
        default: return state;
    };
};
Enter fullscreen mode Exit fullscreen mode

Secondary API Call

The OMDB API was queried again using Redux Thunk in an action creator so that nominations can have complete movie details. (This is needed because when querying for a list of results only a few points of movie-specific data are returned.)

// Searches the API asynchronously 
export const queryOmdbNomination = (movieTitle, movieYear) => {

    return dispatch => {
        dispatch(nominationQueryStarted());

        // OMDB Movie API Query String
        const omdbUrl = `https://www.omdbapi.com/?t=${movieTitle}&y=${movieYear}&apikey=${process.env.REACT_APP_OMDB_KEY}`;

        // API Request
        axios.get(omdbUrl)
            .then((res) => {
                const response = res.data;
                dispatch(nominationQuerySucceeded(response));

            })
            .catch((error) => {
                dispatch(nominationQueryFailed(error));
            })
    }

}
Enter fullscreen mode Exit fullscreen mode

IMDB Links

Nominated films allow you to open their page in IMDB. This is achieved by taking the imdbId and dynamically inserting it into the <a href=""> with it's "" removed.

<a
    className="nom-row__imdb-link"
    href={`https://www.imdb.com/title/${props.imdbID.replace(/['"]+/g, '')}`}
    target="_blank"
    rel="noreferrer noopener"
>
Enter fullscreen mode Exit fullscreen mode

Genre Cut Off

To help control the layout for nominated movies, their genres have been restricted to the first 3.

let updatedGeneres = null;
let propsArray = props.genres.split(" ");

// Shortens generes to 3 items 
if (propsArray.length > 3) {
    updatedGeneres = propsArray.splice(0, 3).join(" ").slice(0, -1);
} else {
    updatedGeneres = props.genres;
}
Enter fullscreen mode Exit fullscreen mode

Local Storage

For a better user experience, nominated movies and the winner preserve their data in local storage. That way nothing goes away when the user refreshes the screen.

The nomination container component looks out newly nominated movies and then stores.

// Pulls Nomination List from local storage
useEffect(() => {

    const localData = localStorage.getItem('nominationList');

    if (localData) {
        setNominationList(action.storeAllNominations(JSON.parse(localData)));
    }

}, [setNominationList])


// Saves resultsArray to local storage
useEffect(() => {
    localStorage.setItem('nominationList', JSON.stringify(nominationList));

}, [nominationList])
Enter fullscreen mode Exit fullscreen mode

When the user is ready to view the winners, the movies are shuffled and this new list is stored in local storage.

// Shuffles the nomination list to pick a winner
shuffle(localStorageList);
localStorage.setItem('winnerList', JSON.stringify(localStorageList));
Enter fullscreen mode Exit fullscreen mode

The winner container component then checks that new local storage list

const winnerList = JSON.parse(localStorage.getItem('winnerList'));
Enter fullscreen mode Exit fullscreen mode

Lastly, both lists are removed from local storage, when the user clicks the reset button.

localStorage.removeItem("winnerList");
localStorage.removeItem("nominationList");
Enter fullscreen mode Exit fullscreen mode

Back To Top


Future Additions

  • Bug: On Firefox, the content sometimes causes sideways scrolling for a few seconds.
  • Animation: Currently I hide overflow for the main wrapper, while the green nomination block comes in, then after a second turn it back on. I would like to find a different solution for this in the future. This can take a hit on performance since overflow applies earlier in the page rendering process, requiring more resources.
  • Sharing: I would like to add a way to share the winner results to social media.

Back To Top

——

Thumbnail designed with Figma

💖 💪 🙅 🚩
gedalyakrycer
Gedalya Krycer

Posted on January 25, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related