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
12 changes: 12 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"presets": [ "es2015", "react" ],
"plugins": [
"transform-object-rest-spread",
"transform-class-properties"
],
"env": {
"development": {
"presets": ["react-hmre"]
}
}
}
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
34 changes: 34 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"plugins": [
"react"
],
"extends": [
"airbnb-base"
],
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true,
"mocha": true
},
"rules": {
"react/jsx-uses-react": 2,
"react/react-in-jsx-scope": 2,
"react/jsx-uses-vars": 2,
"no-underscore-dangle": "off",
"comma-dangle": [
"error",
"never"
],
"import/no-extraneous-dependencies": "off",
"linebreak-style": "off"
}
}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/node_modules
/node_modules
/.dist
83 changes: 80 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,91 @@

Netflix is investing a lot of effort into acquiring and creating beautiful artwork which is used throughout all of member experience on various devices. With a large global catalog and number of different images for a title, managing at scale is important. A non-technical operations team is responsible for acquisition and setup of artwork. Additionally, we have image algorithms that further augment the image metadata to detect actors, background tone etc.

The operations team needs a easy to use user interface that would allow it to quickly manage artwork across titles and sort/group/filter on various attributes (actors, background tone, similarity group etc. As part of this exercise please implement a functional UI that will help solve some of these challenges.
The operations team needs a easy to use user interface that would allow it to quickly manage artwork across titles and sort/group/filter on various attributes (actors, background tone, similarity group etc. As part of this exercise please implement a functional UI that will help solve some of these challenges.

Sample data model (json) is in `mock-data/data-model.json`. You can fill up `server/index.js` to return the json payload and use it in your UI components. We have limited the data model to contain only 1 type of image (“sdp”). You should assume that the service may return large volume of data (upwards of 1000s).
Sample data model (json) is in `server/data/artwork.json`. You can fill up `server/index.js` to return the json payload and use it in your UI components. We have limited the data model to contain only 1 type of image (“sdp”). You should assume that the service may return large volume of data (upwards of 1000s).

## UI should have following features:
## UI should have following features:
* View images (thumbnailURL) which are grouped by an attribute. There are 2 attributes on which user can select to group by - movieId & languageCode. Default would be group by movieId.
* Ability to zoom (fullSizeImageURL) in on a particular image and be presented with details view where all information from the data model is displayed.

### The preferred tech stack is React running on Node.js

Note: Please work and submit the exercise (including tests) similar to how you would send a pull-request to a fellow engineer in the team.

# Netflix Artworks

## Instructions

**Install the dependencies:**

```npm install```

**To run in Development mode use:**

```npm start```

This will start the API Server, bundle the assets (js, jsx) in memory using Webpack and will start webpack-dev-server with hot reload capabilities.

A new window will automatically open in your browser on http://localhost:9000, and all backend calls will be proxied to the API Server on http://localhost:3000.

ESLint will run and tests will be in watch mode.

**To run in Production mode use:**

```npm run build```

This will run eslint, tests and bundle the application in a single file called bundle.js under .dist/.

```npm run server```

This will start the Express server responsible for both serving Application files and Artwork APIs.

Open your browser at http://localhost:3000.

**To run tests use:**

```npm run test```, ```npm run test:watch``` to be in watch mode.

## Application Capabilities

The Netflix Artworks App allows the user to see a list of Artworks paginated, and grouped by a category of his choice, currently supporting movieId and languageCode.

Clicking in one of the Artworks in the list will allow the user to see the Artwork details containing:
* Full size image.
* Movie Id.
* Movie Name.
* Image Type.
* Language Code.

The Artwork details page can also be accessed directly using the route http://SERVER_URL/artwork/:languageCode/:movieId.

For example running in Production mode try accessing: http://localhost:3000/artwork/nl/70140358

**The application is responsive, being usable from smartphones, tablets, laptops and desktops.**

## Application Architecture

The Netflix Artwork Application uses Facebook's ReactJS to build its User Interface, based on Google's Material UI and Express as its WebServer to serve static files and RestAPIs.

The Front-end piece of the Application is implemented using the Flux pattern throughout the state management library Redux.

Application State:
```
{
app: {
groupBy: 'movieId', // artworks default grouped by movieId
offset: 1 // pagination
},
artworks: {}
// artwork object is a map of objects where the key is the grouping
// information. The value is another object normalized by the grouping value
// for fast access to the data.
};
```

Other important libraries:
* react-router for routing.
* webpack for bundling the application.
* babel for transpiling.
* enzyme for component testing.
12 changes: 12 additions & 0 deletions client/actions/actionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Action types constants
**/

// ARTWORK ACTIONS
export const LOAD_ARTWORKS_SUCCESS = 'LOAD_ARTWORKS_SUCCESS';

// APP ACTIONS
export const CHANGE_GROUPING = 'CHANGE_GROUPING';
export const SHOW_MORE_ITEMS = 'SHOW_MORE_ITEMS';
export const SHOW_LESS_ITEMS = 'SHOW_LESS_ITEMS';
export const SHOW_ALL_ITEMS = 'SHOW_ALL_ITEMS';
33 changes: 33 additions & 0 deletions client/actions/appActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as types from './actionTypes';

/**
* @param groupBy
* @return redux action responsible for updating artwork groupBy state.
**/
export function changeGroupBy(groupBy) {
return { type: types.CHANGE_GROUPING, groupBy };
}

/**
* @param groupBy
* @return redux action responsible for increasing artwork offset state.
**/
export function showMoreItems() {
return { type: types.SHOW_MORE_ITEMS };
}

/**
* @param groupBy
* @return redux action responsible for decreasing artwork offset state.
**/
export function showLessItems() {
return { type: types.SHOW_LESS_ITEMS };
}

/**
* @param groupBy
* @return redux action responsible for updating artwork offset state.
**/
export function showAllItems(offset) {
return { type: types.SHOW_ALL_ITEMS, offset };
}
56 changes: 56 additions & 0 deletions client/actions/appActions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import expect from 'expect';
import * as types from './actionTypes';
import * as appActions from './appActions';

// Test a sync action
describe('App Actions', () => {
describe('changeGroupBy', () => {
it('should create a CHANGE_GROUPING action', () => {
const expectedAction = {
type: types.CHANGE_GROUPING,
groupBy: 'movieId'
};

const action = appActions.changeGroupBy('movieId');

expect(action).toEqual(expectedAction);
});
});

describe('showMoreItems', () => {
it('should create a SHOW_MORE_ITEMS action', () => {
const expectedAction = {
type: types.SHOW_MORE_ITEMS
};

const action = appActions.showMoreItems();

expect(action).toEqual(expectedAction);
});
});

describe('showLessItems', () => {
it('should create a SHOW_LESS_ITEMS action', () => {
const expectedAction = {
type: types.SHOW_LESS_ITEMS
};

const action = appActions.showLessItems();

expect(action).toEqual(expectedAction);
});
});

describe('showAllItems', () => {
it('should create a SHOW_ALL_ITEMS action', () => {
const expectedAction = {
type: types.SHOW_ALL_ITEMS,
offset: 5
};

const action = appActions.showAllItems(5);

expect(action).toEqual(expectedAction);
});
});
});
49 changes: 49 additions & 0 deletions client/actions/artworkActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as types from './actionTypes';
import artworkService from '../api/ArtworkService';

/**
* Utility function to check response status.
* @param {response}
* @return {response} in case of success.
**/
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
throw new Error(response.statusText);
}

/**
* Utility function to parse JSON response.
* @param {response}
* @return {JSON object}.
**/
function parseJSON(response) {
return response.json();
}

/**
* @param {artworks}
* @return redux action responsible for updating artwork state.
**/
export function loadArtworksSuccess(artworks) {
return { type: types.LOAD_ARTWORKS_SUCCESS, artworks };
}

/**
* Async action to fetch artworks from the API layer.
* Dispatches LOAD_ARTWORKS_SUCCESS when the artworks are received
* redux action responsible for updating artwork state.
**/
export function loadArtworks() {
return dispatch =>
artworkService.getAllArtworks()
.then(checkStatus)
.then(parseJSON)
.then((artworks) => {
dispatch(loadArtworksSuccess(artworks));
})
.catch((error) => {
throw (error);
});
}
71 changes: 71 additions & 0 deletions client/actions/artworkActions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import expect from 'expect';
import nock from 'nock';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import * as types from './actionTypes';
import * as artworkActions from './artworkActions';

// Test a sync action
describe('Artwork Actions', () => {
const artworks = [{
movieId: 70242311,
movieName: 'Orange Is the New Black',
imageType: 'sdp',
thumbnailUrl: 'http://art.nflximg.net/673e9/b39fcc29b2ac668ee01343de9f21f611c8f673e9.jpg',
fullSizeImageUrl: 'http://art.nflximg.net/78bc7/198343ed941f178d54878aa366a122e4e2e78bc7.jpg',
languageCode: 'it'
}, {
movieId: 70242311,
movieName: 'Orange Is the New Black',
imageType: 'sdp',
thumbnailUrl: 'http://art.nflximg.net/304d2/7da9da4ea90c6df7889b67137cb64737e65304d2.jpg',
fullSizeImageUrl: 'http://art.nflximg.net/934f7/e76e738da86e04d9c388605789211d6a883934f7.jpg',
languageCode: 'de'
}];

describe('loadArtworksSuccess', () => {
it('should create a LOAD_ARTWORKS_SUCCESS action', () => {
const expectedAction = {
type: types.LOAD_ARTWORKS_SUCCESS,
artworks
};

const action = artworkActions.loadArtworksSuccess(artworks);

expect(action).toEqual(expectedAction);
});
});

const middleware = [thunk];
const mockStore = configureMockStore(middleware);

describe('loadArtworks', () => {
beforeEach(() => {
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');
});

afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});

it('should create LOAD_ARTWORKS_SUCCESS when dispatching loadArtworks', (done) => {
nock('http://localhost/api')
.get('/movies')
.reply(200, { body: { artworks } });

const expectedActions = [
{ type: types.LOAD_ARTWORKS_SUCCESS, body: { artworks } }
];

// Testing if success action was called
const store = mockStore({ artworks: [] }, expectedActions);
return store.dispatch(artworkActions.loadArtworks()).then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(types.LOAD_ARTWORKS_SUCCESS);
done();
});
});
});
});
11 changes: 11 additions & 0 deletions client/api/ArtworkService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import RestApi from './RestApi';

/**
* Artwork service Layer for API communication
**/
const artworkApi = new RestApi('/movies');
export default class ArtworkService {
static getAllArtworks() {
return artworkApi.get();
}
}
Loading