diff --git a/reactjs/components/Error.js b/reactjs/components/Error.js new file mode 100644 index 0000000..61d373c --- /dev/null +++ b/reactjs/components/Error.js @@ -0,0 +1,10 @@ +import React from 'react' + +const Error = () => { + return ( +
+

There was an error loading your gifs, please try again later...

+
+ ) +} +export default Error; \ No newline at end of file diff --git a/reactjs/components/GifPages/index.js b/reactjs/components/GifPages/index.js new file mode 100644 index 0000000..aef0c97 --- /dev/null +++ b/reactjs/components/GifPages/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import Gifs from '../../components/Gifs'; + +const GifPages = () => ( +
+ +
+ +
+
+); + +export default GifPages; diff --git a/reactjs/components/HelloFriend/style.scss b/reactjs/components/GifPages/style.scss similarity index 100% rename from reactjs/components/HelloFriend/style.scss rename to reactjs/components/GifPages/style.scss diff --git a/reactjs/components/Gifs.js b/reactjs/components/Gifs.js new file mode 100644 index 0000000..8a078ff --- /dev/null +++ b/reactjs/components/Gifs.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { gifsRequest, gotoNextPage } from '../store/actions/Gifs'; +import Loading from './Loading'; +import Error from './Error'; +import GifData from './gifdata/GifData'; + +class Gifs extends Component { + + state = { + currentPage: 0, + numItemsToDisplay: 12, + response: null, + slicedItems: null, + pageNum: null, + error: null, + totalItems: 0 + } + + + componentDidMount() { + this.props.gifsRequest(); + } + + getSlicedItems = (page, displayItems, response) => { + let start = page * displayItems; + let end = start + displayItems; + let slicedItems = response.slice(start, end); + return slicedItems; + } + + componentWillReceiveProps(nextProps) { + const { currentPage, numItemsToDisplay, response } = this.state; + if (nextProps.response != null) { + let slicedItems = this.getSlicedItems(currentPage, numItemsToDisplay, nextProps.response); + this.setState({ + response: nextProps.response, + slicedItems: slicedItems, + totalItems: nextProps.response.length + }) + } + if (nextProps.pageNum != null) { + let slicedItems = this.getSlicedItems(nextProps.pageNum, numItemsToDisplay, response); + this.setState({ + currentPage: nextProps.pageNum, + slicedItems: slicedItems, + pageNum: nextProps.pageNum + }) + } + } + + render() { + const { response, gotoNextPage, pageNum, error } = this.props; + const { slicedItems, numItemsToDisplay, totalItems, currentPage } = this.state; + + if (response == null && pageNum == null && error == null) { + return ( + + + + ) + } else if ((response !== null || pageNum != null) && error == null) { + return ( + + + + ) + } + else if (response == null && pageNum == null && error != null) { + return ( + + + + ) + } else { + return null; + } + } +} +const mapStateToProps = (state) => ({ + response: state.gifs.response, + error: state.gifs.error, + pageNum: state.gifs.pageNum +}) + +const mapDispatchToProps = { + gifsRequest, gotoNextPage +} + +export default connect(mapStateToProps, mapDispatchToProps)(Gifs); \ No newline at end of file diff --git a/reactjs/components/HelloFriend/index.js b/reactjs/components/HelloFriend/index.js deleted file mode 100644 index e8f3965..0000000 --- a/reactjs/components/HelloFriend/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -const HelloFriend = () => ( -
- Hello, friend. -
- Have a great day :) -
-
-); - -export default HelloFriend; diff --git a/reactjs/components/HtmlHead/index.js b/reactjs/components/HtmlHead/index.js index 3e9d76b..37b4d93 100644 --- a/reactjs/components/HtmlHead/index.js +++ b/reactjs/components/HtmlHead/index.js @@ -22,6 +22,7 @@ const HtmlHead = () => { SystemSeed Technical Assessment + diff --git a/reactjs/components/Loading.js b/reactjs/components/Loading.js new file mode 100644 index 0000000..1aeb379 --- /dev/null +++ b/reactjs/components/Loading.js @@ -0,0 +1,10 @@ +import React from 'react' + +const Loading = () => { + return ( +
+

Loading your gifs...

+
+ ) +} +export default Loading; \ No newline at end of file diff --git a/reactjs/components/gifdata/GifData.js b/reactjs/components/gifdata/GifData.js new file mode 100644 index 0000000..967d8be --- /dev/null +++ b/reactjs/components/gifdata/GifData.js @@ -0,0 +1,16 @@ +import React from 'react'; +import GifGrid from './GifGrid'; +import Pagination from '../pagination/Pagination'; + +const GifData = ({ slicedItems, totalItems, numItemsToDisplay, gotoNextPage }) => { + return ( + + + { + numItemsToDisplay < totalItems && + + } + + ) +} +export default GifData; \ No newline at end of file diff --git a/reactjs/components/gifdata/GifGrid.js b/reactjs/components/gifdata/GifGrid.js new file mode 100644 index 0000000..415f4a4 --- /dev/null +++ b/reactjs/components/gifdata/GifGrid.js @@ -0,0 +1,21 @@ +import React from 'react' + +const GifGrid = ({ slicedItems }) => { + return ( +
+ { + slicedItems.map(item => ( +
+
+ {item.title} +
+
{item.title}
+
+
+
+ )) + } +
+ ) +} +export default GifGrid; \ No newline at end of file diff --git a/reactjs/components/pagination/Pagination.js b/reactjs/components/pagination/Pagination.js new file mode 100644 index 0000000..0d38298 --- /dev/null +++ b/reactjs/components/pagination/Pagination.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import PaginationItem from './PaginationItem'; + +class Pagination extends Component { + + constructor(props) { + super(props); + const { totalItems, numItemsToDisplay } = this.props; + this.totalItems = typeof totalItems === "number" ? totalItems : 0; + this.numItemsToDisplay = typeof numItemsToDisplay === "number" ? numItemsToDisplay : 0; + } + + getPages(totalPage) { + let pages = []; + let i = 0; + + while (i < totalPage) { + pages.push(i); + i++; + } + + return pages; + } + + handleClick = (page, e) => { + e.preventDefault(); + this.props.gotoNextPage(page); + } + + render() { + let pages = this.getPages(Math.ceil(this.totalItems / this.numItemsToDisplay)); + return ( +
+ { + pages.map((page, index) => + this.handleClick(page, e)} /> + ) + } +
+ ) + } +} +export default Pagination; \ No newline at end of file diff --git a/reactjs/components/pagination/PaginationItem.js b/reactjs/components/pagination/PaginationItem.js new file mode 100644 index 0000000..54f2b0f --- /dev/null +++ b/reactjs/components/pagination/PaginationItem.js @@ -0,0 +1,10 @@ +import React from 'react' + +const PaginationItem = ({ page, onClick }) => { + return ( +
+ {page + 1} +
+ ) +} +export default PaginationItem; \ No newline at end of file diff --git a/reactjs/package.json b/reactjs/package.json index fff583c..81f7eb6 100644 --- a/reactjs/package.json +++ b/reactjs/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "axios": "^0.18.0", "compression": "^1.7.3", "express": "^4.16.3", "next": "^6.1.1", diff --git a/reactjs/pages/index.js b/reactjs/pages/index.js index c6b3226..1184602 100644 --- a/reactjs/pages/index.js +++ b/reactjs/pages/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import HelloFriend from '../components/HelloFriend'; +import GifPages from '../components/GifPages'; class Page extends React.Component { static async getInitialProps() { @@ -9,7 +9,7 @@ class Page extends React.Component { } render() { - return ; + return ; } } diff --git a/reactjs/store/actions/Gifs.js b/reactjs/store/actions/Gifs.js new file mode 100644 index 0000000..4efee9b --- /dev/null +++ b/reactjs/store/actions/Gifs.js @@ -0,0 +1,29 @@ +import * as types from './Types'; + +export const gifsRequest = () => { + return { + type: types.GIFS_REQ, + payload: '' + } +} + +export const gifsResponse = (data) => { + return { + type: types.GIFS_RES, + payload: data + } +} + +export const gifsError = (data) => { + return { + type: types.GIFS_ERROR, + payload: data + } +} + +export const gotoNextPage = (pageNum) => { + return { + type: types.GO_NEXT_PAGE, + payload: pageNum + } +} \ No newline at end of file diff --git a/reactjs/store/actions/Types.js b/reactjs/store/actions/Types.js new file mode 100644 index 0000000..b31b101 --- /dev/null +++ b/reactjs/store/actions/Types.js @@ -0,0 +1,5 @@ +export const GIFS_REQ = "GIFS_REQ"; +export const GIFS_RES = "GIFS_RES"; +export const GIFS_ERROR = "GIFS_ERROR"; + +export const GO_NEXT_PAGE = "GO_NEXT_PAGE"; \ No newline at end of file diff --git a/reactjs/store/actions/example.js b/reactjs/store/actions/example.js deleted file mode 100644 index 761019e..0000000 --- a/reactjs/store/actions/example.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Increases custom value in store. - */ -export const increaseValue = () => ({ - type: 'EXAMPLE_ACTION', -}); diff --git a/reactjs/store/api/Api.js b/reactjs/store/api/Api.js new file mode 100644 index 0000000..45b3089 --- /dev/null +++ b/reactjs/store/api/Api.js @@ -0,0 +1,7 @@ +import axios from 'axios'; + +export default { + gif: { + gifs:() => axios.get('http://api.giphy.com/v1/gifs/trending?api_key=ovU9KlXS8RtZr0VgRT6X02ZaH02P30qw&limit=120') + } +} \ No newline at end of file diff --git a/reactjs/store/reducers/GifReducer.js b/reactjs/store/reducers/GifReducer.js new file mode 100644 index 0000000..326d3ee --- /dev/null +++ b/reactjs/store/reducers/GifReducer.js @@ -0,0 +1,29 @@ +import * as types from '../actions/Types'; + +const init = { + response: null, + error: null, + pageNum: null +} + +const gifReducer = (state = init, action) => { + switch (action.type) { + case types.GIFS_REQ: + state = { ...state, response: null, error: null, pageNum: null } + break; + case types.GIFS_RES: + state = { ...state, response: action.payload, error: null, pageNum: null } + break; + case types.GIFS_ERROR: + state = { ...state, response: null, error: action.payload, pageNum: null } + break; + case types.GO_NEXT_PAGE: + state = { ...state, response: null, error: null, pageNum: action.payload } + break; + default: + break; + } + return state; +} + +export default gifReducer; \ No newline at end of file diff --git a/reactjs/store/reducers/example.js b/reactjs/store/reducers/example.js deleted file mode 100644 index de45043..0000000 --- a/reactjs/store/reducers/example.js +++ /dev/null @@ -1,12 +0,0 @@ -export default (state = { count: 0 }, action) => { - switch (action.type) { - case 'EXAMPLE_ACTION': - return { - ...state, - count: state.count + 1, - }; - - default: - return state; - } -}; diff --git a/reactjs/store/reducers/index.js b/reactjs/store/reducers/index.js index 72a8dcf..238d417 100644 --- a/reactjs/store/reducers/index.js +++ b/reactjs/store/reducers/index.js @@ -1,7 +1,7 @@ import { combineReducers } from 'redux'; // Put all your reducers here. -import example from './example'; +import gifReducer from './GifReducer'; export default combineReducers({ - example, -}); + gifs: gifReducer +}); \ No newline at end of file diff --git a/reactjs/store/sagas/GifSaga.js b/reactjs/store/sagas/GifSaga.js new file mode 100644 index 0000000..c9443a7 --- /dev/null +++ b/reactjs/store/sagas/GifSaga.js @@ -0,0 +1,17 @@ +import { call, takeLatest, put } from 'redux-saga/effects'; +import * as types from '../actions/Types'; +import api from '../api/Api'; +import { gifsResponse, gifsError } from '../actions/Gifs'; + +export function* gifSagaWatcher() { + yield takeLatest(types.GIFS_REQ, goReqGif); +} + +function* goReqGif() { + try { + let res = yield call(api.gif.gifs); + yield put(gifsResponse(res.data.data)); + } catch (e) { + yield put(gifsError(e.response)); + } +} diff --git a/reactjs/store/sagas/example.js b/reactjs/store/sagas/example.js deleted file mode 100644 index 2f91211..0000000 --- a/reactjs/store/sagas/example.js +++ /dev/null @@ -1,18 +0,0 @@ -import { all, select, takeEvery } from 'redux-saga/effects'; - -/** - * React on example action being triggered. - */ -function* exampleSaga() { - const counter = yield select(reduxStore => reduxStore.example.count); - yield console.log(`Saga triggered. New counter is ${counter}`); -} - -/** - * Main entry point for all example sagas. - */ -export default function* exampleSagas() { - yield all([ - yield takeEvery('EXAMPLE_ACTION', exampleSaga), - ]); -} diff --git a/reactjs/store/sagas/index.js b/reactjs/store/sagas/index.js index 6ff084d..de38dd8 100644 --- a/reactjs/store/sagas/index.js +++ b/reactjs/store/sagas/index.js @@ -1,9 +1,9 @@ -import { all } from 'redux-saga/effects'; +import { all, fork } from 'redux-saga/effects'; // Put all your sagas here. -import exampleSagas from './example'; +import { gifSagaWatcher } from './GifSaga'; export default function* rootSaga() { yield all([ - ...exampleSagas(), - ]); -} + fork(gifSagaWatcher) + ]) +} \ No newline at end of file diff --git a/reactjs/styles/base.scss b/reactjs/styles/base.scss index 04a626a..357094b 100644 --- a/reactjs/styles/base.scss +++ b/reactjs/styles/base.scss @@ -1,3 +1,7 @@ +$layout_margin: 1%; +$black_clr: #000000; +$pagination_hover: #f4f4f4; + body { font-family: sans-serif; font-size: 16px; @@ -6,4 +10,31 @@ body { margin: 0; padding: 0; min-width: 250px; +} + +.Pagination { + margin: $layout_margin; + display: flex; + flex-flow: row nowrap; + justify-content: center; + width: 100%; + &_Item { + padding: .5%; + position: relative; + cursor: pointer; + min-width: 3rem; + text-align: center; + border:1px solid #ced4da; + color: $black_clr; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + font-size: 1.2rem; + &:hover { + background-color: $pagination_hover; + } + &:active{ + color: #445565 !important; + background-color: #e3e7eb !important; + border-color: #ced4da !important; + } + } } \ No newline at end of file