├── .nvmrc
├── robots.txt
├── public
├── robots.txt
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-48x48.png
├── fonts
│ └── font.woff
├── images
│ ├── lazy.png
│ ├── missing.jpg
│ ├── missing-full.jpg
│ ├── icons
│ │ ├── icon-72x72.png
│ │ ├── icon-96x96.png
│ │ ├── icon-128x128.png
│ │ ├── icon-144x144.png
│ │ ├── icon-152x152.png
│ │ ├── icon-192x192.png
│ │ ├── icon-384x384.png
│ │ ├── icon-512x512.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ └── safari-pinned-tab.svg
│ ├── missing.ff25858a66cc8136c879b91f0dc57d17.jpg
│ ├── search.svg
│ ├── close.svg
│ ├── dots.svg
│ ├── marvel.svg
│ └── github.svg
├── browserconfig.xml
├── app.1d43c609c894bd6b91dc.js.LICENSE.txt
├── manifest.webmanifest
├── sw.js
├── 577.1d43c609c894bd6b91dc.js
├── 788.1d43c609c894bd6b91dc.js
├── 76.1d43c609c894bd6b91dc.js
├── index.html
└── 289.1d43c609c894bd6b91dc.js
├── .eslintignore
├── .env.example
├── src
├── favicon.ico
├── fonts
│ └── font.woff
├── images
│ ├── lazy.png
│ ├── missing.jpg
│ ├── missing-full.jpg
│ ├── icons
│ │ ├── icon-72x72.png
│ │ ├── icon-96x96.png
│ │ ├── icon-128x128.png
│ │ ├── icon-144x144.png
│ │ ├── icon-152x152.png
│ │ ├── icon-192x192.png
│ │ ├── icon-384x384.png
│ │ ├── icon-512x512.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ └── safari-pinned-tab.svg
│ ├── search.svg
│ ├── close.svg
│ ├── dots.svg
│ ├── marvel.svg
│ └── github.svg
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-48x48.png
├── scripts
│ ├── actions
│ │ ├── search.js
│ │ ├── filter.js
│ │ ├── started.js
│ │ ├── menuOpen.js
│ │ ├── fetching.js
│ │ ├── fetchingError.js
│ │ ├── pagination.js
│ │ ├── constants.js
│ │ └── fetch.js
│ ├── reducers
│ │ ├── search.js
│ │ ├── filter.js
│ │ ├── started.js
│ │ ├── menuOpen.js
│ │ ├── fetchingError.js
│ │ ├── fetching.js
│ │ ├── pagination.js
│ │ ├── root.js
│ │ └── data.js
│ ├── model
│ │ ├── initialState.js
│ │ ├── store.js
│ │ ├── api.js
│ │ └── paginationHelper.js
│ ├── components
│ │ ├── scroll-indicator.jsx
│ │ ├── loader.jsx
│ │ ├── error.jsx
│ │ ├── explore.jsx
│ │ ├── back-button.jsx
│ │ ├── cover.jsx
│ │ ├── close-icon.jsx
│ │ ├── pagination-link.jsx
│ │ ├── infos.jsx
│ │ ├── grid-item.jsx
│ │ ├── transition.jsx
│ │ ├── menu.jsx
│ │ ├── header.jsx
│ │ ├── grid.jsx
│ │ ├── search.jsx
│ │ └── pagination.jsx
│ ├── app.jsx
│ ├── container
│ │ ├── detailComicContainer.jsx
│ │ ├── detailCharacterContainer.jsx
│ │ └── homeContainer.jsx
│ ├── pages
│ │ ├── app.jsx
│ │ ├── detailsCommon.js
│ │ ├── about.jsx
│ │ ├── home.jsx
│ │ ├── detailCharacter.jsx
│ │ └── detailComic.jsx
│ └── misc
│ │ └── tilt.js
├── scss
│ ├── media-queries
│ │ ├── tablet.scss
│ │ ├── wide.scss
│ │ └── desktop.scss
│ ├── components
│ │ ├── grid.scss
│ │ ├── error.scss
│ │ ├── offline-ready.scss
│ │ ├── loading.scss
│ │ ├── menu.scss
│ │ ├── back-button.scss
│ │ ├── filter.scss
│ │ ├── search.scss
│ │ ├── scroll-indicator.scss
│ │ ├── header.scss
│ │ ├── slides.scss
│ │ ├── pagination.scss
│ │ └── thumb.scss
│ ├── base
│ │ ├── easing.scss
│ │ ├── color.scss
│ │ └── reset.scss
│ ├── app.scss
│ └── pages
│ │ ├── home.scss
│ │ ├── about.scss
│ │ └── detail.scss
├── browserconfig.xml
├── manifest.webmanifest
└── index.html
├── api.keys.json
├── .gitignore
├── .github
├── dependabot.yml
└── workflows
│ ├── semgrep.yml
│ ├── deploy.yml
│ ├── build.yml
│ └── codeql-analysis.yml
├── .stylelintrc
├── test
├── model
│ ├── initialState.test.js
│ ├── api.test.js
│ └── paginationHelper.test.js
└── misc
│ └── tilt.test.js
├── .editorconfig
├── .codeclimate.yml
├── humans.txt
├── webpack.dev.js
├── .babelrc
├── webpack.prod.js
├── crossdomain.xml
├── .eslintrc.json
├── sass-lint.yml
├── workbox-config.js
├── LICENSE
├── 404.html
├── README.md
├── package.json
└── webpack.common.js
/.nvmrc:
--------------------------------------------------------------------------------
1 | 15.11
--------------------------------------------------------------------------------
/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.json
2 | /public/
3 | /coverage/
4 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PUBLIC_API_KEY=####
2 | PUBLIC_HASH=####
3 | NODE_ENV=production
4 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/fonts/font.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/fonts/font.woff
--------------------------------------------------------------------------------
/src/images/lazy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/lazy.png
--------------------------------------------------------------------------------
/src/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/favicon-16x16.png
--------------------------------------------------------------------------------
/src/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/favicon-32x32.png
--------------------------------------------------------------------------------
/src/favicon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/favicon-48x48.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/favicon-48x48.png
--------------------------------------------------------------------------------
/public/fonts/font.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/fonts/font.woff
--------------------------------------------------------------------------------
/public/images/lazy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/lazy.png
--------------------------------------------------------------------------------
/src/images/missing.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/missing.jpg
--------------------------------------------------------------------------------
/public/images/missing.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/missing.jpg
--------------------------------------------------------------------------------
/api.keys.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "https://gateway.marvel.com",
3 | "version": "v1",
4 | "folder": "public"
5 | }
6 |
--------------------------------------------------------------------------------
/src/images/missing-full.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/missing-full.jpg
--------------------------------------------------------------------------------
/public/images/missing-full.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/missing-full.jpg
--------------------------------------------------------------------------------
/src/images/icons/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-72x72.png
--------------------------------------------------------------------------------
/src/images/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-96x96.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | nodesource_setup.sh
4 | secret.json
5 | out/chrome
6 | src/images/Thumbs.db
7 | .env
8 |
--------------------------------------------------------------------------------
/public/images/icons/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-72x72.png
--------------------------------------------------------------------------------
/public/images/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-96x96.png
--------------------------------------------------------------------------------
/src/images/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-128x128.png
--------------------------------------------------------------------------------
/src/images/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-144x144.png
--------------------------------------------------------------------------------
/src/images/icons/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-152x152.png
--------------------------------------------------------------------------------
/src/images/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-192x192.png
--------------------------------------------------------------------------------
/src/images/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-384x384.png
--------------------------------------------------------------------------------
/src/images/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/icon-512x512.png
--------------------------------------------------------------------------------
/public/images/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-128x128.png
--------------------------------------------------------------------------------
/public/images/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-144x144.png
--------------------------------------------------------------------------------
/public/images/icons/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-152x152.png
--------------------------------------------------------------------------------
/public/images/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-192x192.png
--------------------------------------------------------------------------------
/public/images/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-384x384.png
--------------------------------------------------------------------------------
/public/images/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/icon-512x512.png
--------------------------------------------------------------------------------
/src/images/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/public/images/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/src/images/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/images/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/scripts/actions/search.js:
--------------------------------------------------------------------------------
1 | export default function filter(text) {
2 | return {
3 | type: 'SEARCH',
4 | text
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/images/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/images/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/src/images/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/scripts/actions/filter.js:
--------------------------------------------------------------------------------
1 | export default function filter(filter) {
2 | return {
3 | type: 'FILTER',
4 | filter
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/scripts/actions/started.js:
--------------------------------------------------------------------------------
1 | export default function started(start) {
2 | return {
3 | type: 'STARTED',
4 | start
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/images/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/scripts/actions/menuOpen.js:
--------------------------------------------------------------------------------
1 | export default function menuOpen(open) {
2 | return {
3 | type: 'MENU_TOOGLE',
4 | open
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/scripts/actions/fetching.js:
--------------------------------------------------------------------------------
1 | export default function fetching(fetching) {
2 | return {
3 | type: 'FETCHING',
4 | fetching
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/scss/media-queries/tablet.scss:
--------------------------------------------------------------------------------
1 | @media screen and (min-width: 650px ) {
2 | .detail {
3 | img {
4 | max-width: 55%;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/scss/media-queries/wide.scss:
--------------------------------------------------------------------------------
1 | @media screen and (min-width: 1200px ) {
2 | .detail {
3 | img {
4 | max-width: 60vh;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/scripts/actions/fetchingError.js:
--------------------------------------------------------------------------------
1 | export default function fetchingError(error) {
2 | return {
3 | type: 'FETCHING_ERROR',
4 | error
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/src/scripts/actions/pagination.js:
--------------------------------------------------------------------------------
1 | export default function pagination(pagination) {
2 | return {
3 | type: 'PAGINATION',
4 | pagination
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/missing.ff25858a66cc8136c879b91f0dc57d17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iondrimba/marvel-api-explorer/HEAD/public/images/missing.ff25858a66cc8136c879b91f0dc57d17.jpg
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "09:00"
8 | open-pull-requests-limit: 1
9 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "rules": {
4 | "indentation": 2,
5 | "no-missing-end-of-source-newline": true,
6 | "number-leading-zero":"never"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/model/initialState.test.js:
--------------------------------------------------------------------------------
1 | import initialState from '../../src/scripts/model/initialState';
2 |
3 | describe('initialState', () => {
4 | it('initial state should be an object', () => {
5 | expect(typeof initialState).toBe('object');
6 | })
7 | });
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*.js]
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
--------------------------------------------------------------------------------
/src/scss/components/grid.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | @import '../base/easing';
3 |
4 | .grid {
5 | display: flex;
6 | flex-direction: row;
7 | flex-wrap: wrap;
8 | margin-bottom: 16px;
9 | margin-top: 50px;
10 | padding: 10px;
11 | }
12 |
--------------------------------------------------------------------------------
/src/scripts/reducers/search.js:
--------------------------------------------------------------------------------
1 | import { SEARCH } from '../actions/constants';
2 |
3 | function search(state = '', action) {
4 | switch (action.type) {
5 | case SEARCH:
6 | return action.text;
7 | }
8 | return state;
9 | }
10 |
11 | export default search;
12 |
--------------------------------------------------------------------------------
/src/images/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/scripts/reducers/filter.js:
--------------------------------------------------------------------------------
1 | import { FILTER } from '../actions/constants';
2 |
3 | function filter(state = '', action) {
4 | switch (action.type) {
5 | case FILTER:
6 | return action.filter;
7 | }
8 | return state;
9 | }
10 |
11 | export default filter;
12 |
--------------------------------------------------------------------------------
/src/scss/base/easing.scss:
--------------------------------------------------------------------------------
1 | $easings: (
2 | easeOutExpo: cubic-bezier(.19, 1, .22, 1),
3 | easeOutQuart: cubic-bezier(.165, .84, .44, 1),
4 | easeOutCirc: cubic-bezier(.075, .82, .165, 1)
5 | );
6 |
7 | @function easing($name) {
8 | @return map-get($easings, $name);
9 | }
10 |
--------------------------------------------------------------------------------
/public/images/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/scripts/reducers/started.js:
--------------------------------------------------------------------------------
1 | import { STARTED } from '../actions/constants';
2 |
3 | function started(state = false, action) {
4 | switch (action.type) {
5 | case STARTED:
6 | return action.start;
7 | }
8 | return state;
9 | }
10 |
11 | export default started;
12 |
--------------------------------------------------------------------------------
/src/images/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/images/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/scripts/reducers/menuOpen.js:
--------------------------------------------------------------------------------
1 | import { MENU_TOOGLE } from '../actions/constants';
2 |
3 | function menuOpen(state = false, action) {
4 | switch (action.type) {
5 | case MENU_TOOGLE:
6 | return action.open;
7 | }
8 | return state;
9 | }
10 |
11 | export default menuOpen;
12 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ratings:
2 | paths:
3 | - "src/**/*.js"
4 | - "src/**/*.jsx"
5 | - "src/**/*.scss"
6 | exclude_paths:
7 | - "spec/**/*"
8 | - "public/**/*"
9 | - "coverage/**/*"
10 | - "karma.conf.js"
11 | - "test.webpack.config.js"
12 | - "webpack.config.js"
13 | - "workbox-config.js"
14 |
--------------------------------------------------------------------------------
/src/scripts/reducers/fetchingError.js:
--------------------------------------------------------------------------------
1 | import { FETCHING_ERROR } from '../actions/constants';
2 |
3 | function fetchingError(state = '', action) {
4 | switch (action.type) {
5 | case FETCHING_ERROR:
6 | return action.error;
7 | }
8 | return state;
9 | }
10 |
11 | export default fetchingError;
12 |
--------------------------------------------------------------------------------
/src/scss/components/error.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 |
3 | .error {
4 | background-color: color(error);
5 | display: block;
6 | left: 50%;
7 | padding: 20px;
8 | position: absolute;
9 | text-align: center;
10 | top: 50%;
11 | transform: translate(-50%, -50%);
12 | width: 100%;
13 | }
14 |
--------------------------------------------------------------------------------
/src/scripts/model/initialState.js:
--------------------------------------------------------------------------------
1 | const defaultStore = {
2 | fetching: false,
3 | pagination: { current: 1, total: 0, pages: [], next: false, prev: false },
4 | filter: 'comics',
5 | search: '',
6 | started: false,
7 | error: { code: '' },
8 | menuOpen: false,
9 | data: []
10 | };
11 |
12 | export default defaultStore;
13 |
--------------------------------------------------------------------------------
/humans.txt:
--------------------------------------------------------------------------------
1 | # humanstxt.org/
2 | # The humans responsible & technology colophon
3 |
4 | # TEAM
5 |
6 | Ion Drimba Filho Developer/Designer iondrimba@gmail.com
7 |
8 | # THANKS
9 |
10 | B2 (UX Tips)
11 | Bulldog (Interface Tips)
12 |
13 | # TECHNOLOGY COLOPHON
14 |
15 | CSS3, HTML5, JS, React, Redux, Service Worker
16 | ES6, Babel, Webpack
17 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const path = require('path');
3 | const common = require('./webpack.common.js');
4 |
5 | module.exports = merge(common, {
6 | mode: 'development',
7 | output: {
8 | publicPath: 'http://localhost:8080/',
9 | },
10 | devServer: {
11 | contentBase: path.join(__dirname, 'public')
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/react",
5 | ],
6 | "plugins": [
7 | "@babel/plugin-syntax-dynamic-import"
8 | ],
9 | "sourceMap": "inline",
10 | "env": {
11 | "testing": {
12 | "sourceMap": "inline"
13 | },
14 | "test": {
15 | "plugins": ["@babel/plugin-transform-runtime"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/scripts/actions/constants.js:
--------------------------------------------------------------------------------
1 | export const FETCHING = 'FETCHING';
2 | export const FETCHING_ERROR = 'FETCHING_ERROR';
3 | export const CHANGED_PAGE = 'CHANGED_PAGE';
4 | export const FETCHED = 'FETCHED';
5 | export const PAGINATION = 'PAGINATION';
6 | export const FILTER = 'FILTER';
7 | export const SEARCH = 'SEARCH';
8 | export const STARTED = 'STARTED';
9 | export const MENU_TOOGLE = 'MENU_TOOGLE';
10 |
--------------------------------------------------------------------------------
/src/scripts/reducers/fetching.js:
--------------------------------------------------------------------------------
1 | import { FETCHING, FETCHED, FETCHING_ERROR } from '../actions/constants';
2 |
3 | function fetching(state = false, action) {
4 | switch (action.type) {
5 | case FETCHING:
6 | return true;
7 | case FETCHING_ERROR:
8 | return false;
9 | case FETCHED:
10 | return false;
11 | }
12 | return state;
13 | }
14 |
15 | export default fetching;
16 |
--------------------------------------------------------------------------------
/src/scripts/components/scroll-indicator.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 |
3 | class ScrollIndicator extends PureComponent {
4 | render() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 | }
12 |
13 | export default ScrollIndicator;
14 |
--------------------------------------------------------------------------------
/src/images/dots.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/images/dots.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/scripts/components/loader.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Loader extends PureComponent {
5 | render() {
6 | return (
7 | this.props.fetching ?
:
8 | );
9 | }
10 | }
11 |
12 | Loader.propTypes = {
13 | fetching: PropTypes.bool,
14 | }
15 |
16 | export default Loader;
17 |
--------------------------------------------------------------------------------
/src/scripts/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { render } from 'react-dom';
4 | import { Router } from 'react-router-dom';
5 | import store from './model/store';
6 | import { history } from './model/store';
7 | import App from './pages/app';
8 |
9 | render(
10 |
11 |
12 |
13 |
14 | ,
15 | document.querySelector('.marvel-app')
16 | );
17 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
4 | const common = require('./webpack.common.js');
5 |
6 | module.exports = merge(common, {
7 | mode: 'production',
8 | output: {
9 | publicPath: '/',
10 | },
11 | plugins: [
12 | new MiniCssExtractPlugin({
13 | filename: './css/[name].[hash].css',
14 | }),
15 | new CleanWebpackPlugin(),
16 | ]
17 | });
18 |
--------------------------------------------------------------------------------
/src/scripts/components/error.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Error extends PureComponent {
5 | onRetry() {
6 | this.props.retry();
7 | }
8 | render() {
9 | return (
10 | this.props.error.code ?
Click to retry please :) :
11 | );
12 | }
13 | }
14 | Error.propTypes = {
15 | error: PropTypes.object,
16 | retry: PropTypes.func,
17 | }
18 | export default Error;
19 |
--------------------------------------------------------------------------------
/src/scripts/components/explore.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Explore extends PureComponent {
5 | render() {
6 | return (
7 | this.props.fetching || this.props.started ?
: start exploring
8 | );
9 | }
10 | }
11 |
12 | Explore.propTypes = {
13 | fetching: PropTypes.bool,
14 | started: PropTypes.bool,
15 | onClick: PropTypes.func
16 | }
17 |
18 | export default Explore;
19 |
--------------------------------------------------------------------------------
/src/scss/components/offline-ready.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | @import '../base/easing';
3 |
4 | .alert-offline {
5 | background-color: color(gradient-primary);
6 | color: color(invert);
7 | display: block;
8 | font-weight: bold;
9 | padding: 20px;
10 | position: absolute;
11 | text-align: center;
12 | top: 0;
13 | transform: translateY(0) translateZ(0);
14 | transition: transform .3s easing(easeOutExpo);
15 | width: 100%;
16 | z-index: 1;
17 |
18 | &.show-offlline-ready {
19 | transform: translateY(50px) translateZ(0);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/scripts/components/back-button.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class BackButton extends PureComponent {
5 | constructor(props) {
6 | super(props);
7 | }
8 | render() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | BackButton.propTypes = {
18 | onClick: PropTypes.func
19 | }
20 |
21 | export default BackButton;
22 |
--------------------------------------------------------------------------------
/src/scripts/components/cover.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Cover extends PureComponent {
5 | render() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 | }
15 |
16 | Cover.propTypes = {
17 | selectedItem: PropTypes.object,
18 | }
19 |
20 | export default Cover;
21 |
--------------------------------------------------------------------------------
/src/scss/components/loading.scss:
--------------------------------------------------------------------------------
1 | .loading {
2 | animation: yoyo .8s infinite;
3 | left: 50%;
4 | max-width: 100px;
5 | position: absolute;
6 | top: 50%;
7 | transform: translate(-50%, -50%) translate3d(0, 0, 0);
8 | transform-origin: top center;
9 | width: 20%;
10 | }
11 |
12 | @keyframes yoyo {
13 | from {
14 | transform: translate(-50%, -50%) translate3d(0, 0, 0);
15 | }
16 |
17 | 50% {
18 | transform: translate(-50%, -50%) scale(1.2) translate3d(0, 0, 0);
19 | }
20 |
21 | to {
22 | transform: translate(-50%, -50%) translate3d(0, 0, 0);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/images/marvel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/scss/components/menu.scss:
--------------------------------------------------------------------------------
1 |
2 | @import '../base/color';
3 |
4 | .menu {
5 | background-color: color(menu);
6 | color: color(invert);
7 | display: flex;
8 | position: relative;
9 | text-align: center;
10 | text-transform: lowercase;
11 | width: 180px;
12 |
13 | &.show {
14 | box-shadow: 6px -1px 20px 4px color(shadow);
15 | }
16 |
17 | button {
18 | align-self: center;
19 | background-color: inherit;
20 | border: 0;
21 | color: inherit;
22 | cursor: pointer;
23 | display: block;
24 | height: 100%;
25 | padding: 10px;
26 | width: 100%;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/public/images/marvel.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/images/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/images/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.github/workflows/semgrep.yml:
--------------------------------------------------------------------------------
1 | on:
2 | workflow_dispatch: {}
3 | pull_request: {}
4 | push:
5 | branches:
6 | - main
7 | - master
8 | paths:
9 | - .github/workflows/semgrep.yml
10 | schedule:
11 | # random HH:MM to avoid a load spike on GitHub Actions at 00:00
12 | - cron: 53 3 * * *
13 | name: Semgrep
14 | jobs:
15 | semgrep:
16 | name: semgrep/ci
17 | runs-on: ubuntu-latest
18 | permissions:
19 | contents: read
20 | env:
21 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
22 | container:
23 | image: semgrep/semgrep
24 | steps:
25 | - uses: actions/checkout@v4
26 | - run: semgrep ci
27 |
--------------------------------------------------------------------------------
/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/src/scripts/components/close-icon.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class CloseIcon extends PureComponent {
5 | render() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 | }
15 |
16 | CloseIcon.propTypes = {
17 | color: PropTypes.string
18 | }
19 |
20 | export default CloseIcon;
21 |
--------------------------------------------------------------------------------
/src/scripts/container/detailComicContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { withRouter } from 'react-router-dom'
3 | import DetailComic from '../pages/detailComic';
4 | import { createSelector } from 'reselect';
5 |
6 | const getItem = (state, props) => {
7 | return state.find((item, i) => item.id === Number(props.match.params.id));
8 | }
9 |
10 | const getSelectedItem = createSelector(
11 | [getItem],
12 | (item, props) => item
13 | )
14 |
15 | const mapStateToProps = (state, props) => {
16 | return {
17 | selectedItem: getSelectedItem(state.data, props)
18 | }
19 | }
20 |
21 | export default withRouter(connect(mapStateToProps, null)(DetailComic));
22 |
--------------------------------------------------------------------------------
/src/scripts/reducers/pagination.js:
--------------------------------------------------------------------------------
1 | import { PAGINATION } from '../actions/constants';
2 | import { LOCATION_CHANGE } from 'react-router-redux';
3 |
4 | function pagination(state = { current: 1, total: 0, pages: [], next: false, prev: false }, action) {
5 | switch (action.type) {
6 | case LOCATION_CHANGE:
7 | var page = action.payload.location.pathname.split('/')[2];
8 |
9 | if (isNaN(page)) {
10 | return state;
11 | }
12 | return Object.assign({}, state, { current: Number(page) });
13 |
14 | case PAGINATION:
15 | return Object.assign({}, state, action.pagination);
16 | }
17 | return state;
18 | }
19 |
20 | export default pagination;
21 |
--------------------------------------------------------------------------------
/src/scripts/components/pagination-link.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class PaginationLink extends PureComponent {
5 | render() {
6 | return (
7 |
8 | {this.props.label}
9 |
10 | );
11 | }
12 | }
13 |
14 | PaginationLink.propTypes = {
15 | className: PropTypes.string,
16 | iconClassName: PropTypes.string,
17 | href: PropTypes.string,
18 | label: PropTypes.string,
19 | onClick: PropTypes.func
20 | }
21 |
22 | export default PaginationLink;
23 |
--------------------------------------------------------------------------------
/src/scripts/container/detailCharacterContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { withRouter } from 'react-router-dom'
3 | import DetailCharacter from '../pages/detailCharacter';
4 | import { createSelector } from 'reselect';
5 |
6 | const getItem = (state, props) => {
7 | return state.find((item, i) => item.id === Number(props.match.params.id));
8 | }
9 |
10 | const getSelectedItem = createSelector(
11 | [getItem],
12 | (item, props) => item
13 | )
14 |
15 | const mapStateToProps = (state, props) => {
16 | return {
17 | selectedItem: getSelectedItem(state.data, props)
18 | }
19 | }
20 |
21 | export default withRouter(connect(mapStateToProps, null)(DetailCharacter));
22 |
--------------------------------------------------------------------------------
/src/scss/app.scss:
--------------------------------------------------------------------------------
1 | @import './base/reset';
2 | @import './components/slides';
3 | @import './components/filter';
4 | @import './components/header';
5 | @import './components/search';
6 | @import './components/grid';
7 | @import './components/back-button';
8 | @import './components/scroll-indicator';
9 | @import './components/thumb';
10 | @import './components/loading';
11 | @import './components/error';
12 | @import './components/pagination';
13 | @import './components/offline-ready';
14 | @import './components/menu';
15 | @import './pages/about';
16 | @import './pages/detail';
17 | @import './pages/home';
18 | @import './media-queries/tablet';
19 | @import './media-queries/desktop';
20 | @import './media-queries/wide';
21 |
--------------------------------------------------------------------------------
/src/scripts/reducers/root.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { connectRouter } from 'connected-react-router';
3 | import data from './data';
4 | import fetching from './fetching';
5 | import fetchingError from './fetchingError';
6 | import filter from './filter';
7 | import search from './search';
8 | import pagination from './pagination';
9 | import menuOpen from './menuOpen';
10 | import started from './started';
11 |
12 | const RootReducer = (history) => combineReducers({
13 | fetching,
14 | error: fetchingError,
15 | filter,
16 | started,
17 | search,
18 | pagination,
19 | router: connectRouter(history),
20 | menuOpen,
21 | data
22 | });
23 |
24 | export default RootReducer;
25 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "PRODUCTION": true
4 | },
5 | "plugins": [
6 | "react",
7 | "jest"
8 | ],
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:react/recommended"
12 | ],
13 | "parserOptions": {
14 | "ecmaVersion": 7,
15 | "sourceType": "module",
16 | "ecmaFeatures": {
17 | "jsx": true
18 | }
19 | },
20 | "parser": "babel-eslint",
21 | "env": {
22 | "es6": true,
23 | "browser": true,
24 | "node": true,
25 | "jest/globals": true
26 | },
27 | "rules": {
28 | "eqeqeq": 1,
29 | "no-unused-vars": 0,
30 | "no-console": 0,
31 | "react/no-deprecated": 0,
32 | "curly": 1,
33 | "quotes": [
34 | 1,
35 | "single"
36 | ]
37 | }
38 | }
--------------------------------------------------------------------------------
/src/scripts/components/infos.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Infos extends PureComponent {
5 | render() {
6 | return (
7 |
8 | {this.props.title}
9 |
10 | {
11 | this.props.data.map((item, index) => {
12 | return {item.name}
13 | })
14 | }
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | Infos.propTypes = {
22 | type: PropTypes.string,
23 | title: PropTypes.string,
24 | data: PropTypes.array
25 | }
26 |
27 | export default Infos;
28 |
--------------------------------------------------------------------------------
/src/scripts/model/store.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 | import { routerMiddleware } from 'connected-react-router';
3 | import { createStore, applyMiddleware, compose } from 'redux';
4 | import thunk from 'redux-thunk';
5 | import RootReducer from '../reducers/root';
6 | import defaultStore from './initialState';
7 | import Api from './api';
8 |
9 | export const history = createBrowserHistory();
10 |
11 | const api = new Api('13065ce22cdecaf8358b1b56dc54e2c7', 'bff1bb03adcde1c4dcb3417d64511e0b');
12 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
13 |
14 | export default createStore(RootReducer(history), defaultStore,
15 | composeEnhancers(
16 | applyMiddleware(routerMiddleware(history), thunk.withExtraArgument(api))
17 | ));
18 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: deploy
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 |
10 | jobs:
11 | deploy:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [15.11.0]
16 |
17 | steps:
18 | - name: SSH Deploy
19 | uses: garygrossgarten/github-action-ssh@release
20 | with:
21 | command: cd ${{ secrets.FOLDER }} && git pull origin main
22 | host: ${{ secrets.HOST }}
23 | username: ${{ secrets.USER_NAME }}
24 | privateKey: ${{ secrets.PRIVATE_KEY}}
25 |
--------------------------------------------------------------------------------
/src/scss/base/color.scss:
--------------------------------------------------------------------------------
1 | $primary: #bd1023;
2 | $scondary: #4e4e4e;
3 | $colors: (
4 | primary: $primary,
5 | primary-lighter: #ff0000,
6 | error: $primary,
7 | invert: #ffffff,
8 | secondary: #333333,
9 | darkest: #000000,
10 | secondary-lighter: $scondary,
11 | gradient-primary: #1f1f1f,
12 | gradient-secondary: #1b1a1a,
13 | filter: #5a5858,
14 | filter-primary: #444343,
15 | filter-secondary: $scondary,
16 | mask: $primary,
17 | header: $primary,
18 | menu: $scondary,
19 | pagination: $primary,
20 | pagination-selected: #191919,
21 | shadow: rgba(0, 0, 0, .75),
22 | info: $scondary,
23 | series: #edeeef,
24 | series-secondary: #dcdcdc,
25 | stories: #676767,
26 | stories-secondary: #565656,
27 | );
28 |
29 | @function color($name) {
30 | @return map-get($colors, $name);
31 | }
32 |
--------------------------------------------------------------------------------
/src/scss/components/back-button.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | $back-button-size: 44px;
3 | $back-button-icon-size: 10px;
4 |
5 | .back-button {
6 | align-items: center;
7 | background-color: transparent;
8 | border: 0;
9 | cursor: pointer;
10 | display: flex;
11 | height: $back-button-size;
12 | justify-content: center;
13 | opacity: 1;
14 | position: fixed;
15 | width: $back-button-size;
16 | z-index: 2;
17 |
18 | &__icon {
19 | border-bottom: 0;
20 | border-left: 2px solid color(invert);
21 | border-right: 0;
22 | border-top: 2px solid color(invert);
23 | display: block;
24 | height: $back-button-icon-size;
25 | left: 40%;
26 | margin: 0;
27 | padding: 0;
28 | position: absolute;
29 | transform: rotate(-45deg);
30 | width: $back-button-icon-size;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/scripts/reducers/data.js:
--------------------------------------------------------------------------------
1 | import { FETCHED, FETCHING } from '../actions/constants';
2 |
3 | function data(state = [], action) {
4 | switch (action.type) {
5 | case FETCHING:
6 | return [];
7 | case FETCHED:
8 | var filtered = [...action.data.data.data.results].map((item) => {
9 | var thumb = '/images/missing.jpg',
10 | full = '/images/missing-full.jpg';
11 |
12 | if (!item.thumbnail.path.includes('image_not_available')) {
13 | thumb = `${item.thumbnail.path}/portrait_incredible.${item.thumbnail.extension}`;
14 | full = `${item.thumbnail.path}.${item.thumbnail.extension}`;
15 | }
16 |
17 | item.thumb = thumb;
18 | item.full = full;
19 |
20 | return item;
21 | });
22 |
23 | return filtered;
24 | }
25 | return state;
26 | }
27 |
28 | export default data;
29 |
--------------------------------------------------------------------------------
/src/scripts/components/grid-item.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Link } from 'react-router-dom'
3 | import PropTypes from 'prop-types';
4 |
5 | class GridItem extends PureComponent {
6 | render() {
7 | return (
8 |
9 |
10 |
11 | {this.props.title}
12 |
13 | );
14 | }
15 | }
16 |
17 | GridItem.propTypes = {
18 | id: PropTypes.number,
19 | thumb: PropTypes.string,
20 | title: PropTypes.string,
21 | filter: PropTypes.string
22 | }
23 |
24 | export default GridItem;
25 |
--------------------------------------------------------------------------------
/src/scss/pages/home.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 |
3 | .home {
4 | background: linear-gradient(to bottom, color(gradient-primary), color(gradient-secondary));
5 | min-height: 100vh;
6 | padding-bottom: 25px;
7 |
8 | form {
9 | button[type='submit'] {
10 | display: none;
11 | }
12 | }
13 |
14 | .btn-explore {
15 | position: absolute;
16 | display: block;
17 | width: auto;
18 | cursor: pointer;
19 | color: color(secondary);
20 | top: 50%;
21 | left: 50%;
22 | transform: translate(-50%, -50%);
23 | font-weight: bolder;
24 | background-color: color(invert);
25 | padding: 10px 20px;
26 | border-radius: 80px;
27 | text-align: center;
28 | text-transform: uppercase;
29 | box-shadow: -1px 1px 20px 13px rgba(0, 0, 0, 0.15);
30 |
31 | &:active {
32 | box-shadow: -1px 1px 20px 13px rgba(0, 0, 0, 0);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/scripts/components/transition.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Transition = () => (
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default Transition;
15 |
--------------------------------------------------------------------------------
/src/scss/components/filter.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 |
3 | .list {
4 | background-color: color(filter);
5 | border-radius: 0 0 4px 4px;
6 | color: color(invert);
7 | float: right;
8 | left: 0;
9 | position: absolute;
10 | text-transform: lowercase;
11 | top: 45px;
12 | width: 100%;
13 |
14 | &__item {
15 | cursor: pointer;
16 | padding: 5px 10px;
17 |
18 | span {
19 | display: block;
20 | width: 100%;
21 | }
22 |
23 | &:nth-child(odd) {
24 | background-color: color(filter-primary);
25 | }
26 |
27 | &:nth-child(even) {
28 | background-color: color(filter-secondary);
29 | }
30 |
31 | &:last-child {
32 | border-radius: 0 0 4px 4px;
33 | }
34 | }
35 |
36 | &.show {
37 | box-shadow: -1px 12px 20px 0 color(shadow);
38 | display: block;
39 | }
40 |
41 | &.hide {
42 | display: none;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/scss/base/reset.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 |
3 | @font-face {
4 | font-family: 'font';
5 | src: url('../fonts/font.woff') format('woff');
6 | font-style: normal;
7 | font-weight: normal;
8 | font-display: swap;
9 | }
10 |
11 | * {
12 | box-sizing: border-box;
13 | }
14 |
15 | input {
16 | font-family: inherit;
17 | }
18 |
19 | html {
20 | -webkit-tap-highlight-color: transparent;
21 | overflow-y: auto;
22 | touch-action: manipulation;
23 | }
24 |
25 | body, html {
26 | background: linear-gradient(to bottom, color(gradient-primary), color(gradient-secondary));
27 | font-family: 'font', sans-serif;
28 | font-size: 16px;
29 | height: 100%;
30 | margin: 0;
31 | padding: 0;
32 | }
33 |
34 | h1, h2, h3, ul, button {
35 | font-size: inherit;
36 | margin: 0;
37 | padding: 0;
38 | }
39 |
40 | ul {
41 | list-style-type: none;
42 | margin: 0;
43 | padding: 0;
44 | padding-bottom: 20px;
45 | }
46 |
--------------------------------------------------------------------------------
/src/scripts/actions/fetch.js:
--------------------------------------------------------------------------------
1 | import pagination from './pagination';
2 | import fetchingError from './fetchingError';
3 |
4 | export function fetched(data) {
5 | return {
6 | type: 'FETCHED',
7 | data
8 | };
9 | }
10 |
11 | export function fetch(options) {
12 | return function (dispatch, getState, api) {
13 | return api.get(options).then((data) => {
14 | dispatch(fetched(data));
15 |
16 | const { limit, total } = data.data.data;
17 | const pages = Math.round(total / limit);
18 |
19 | if (getState().pagination.total !== pages && options.page) {
20 | const current = options.page > pages ? 1 : options.page;
21 | const pg = Object.assign({}, getState().pagination, { current, total: pages });
22 |
23 | dispatch(pagination(pg));
24 | }
25 |
26 | }, (reject) => {
27 | dispatch(fetchingError(reject));
28 |
29 | }).catch(function (reason) {
30 | dispatch(fetchingError(reason));
31 | })
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/sass-lint.yml:
--------------------------------------------------------------------------------
1 | #########################
2 | ## Sample Sass Lint File
3 | #########################
4 | # Linter Options
5 | options:
6 | # Don't merge default rules
7 | merge-default-rules: false
8 | # Set the formatter to 'html'
9 |
10 | max-warnings: 50
11 | # File Options
12 | files:
13 | include: '**/*.s+(a|c)ss'
14 | ignore:
15 | - 'vendor/**/*.*'
16 | # Rule Configuration
17 | rules:
18 | extends-before-mixins: 2
19 | extends-before-declarations: 2
20 | placeholder-in-extend: 2
21 | mixins-before-declarations:
22 | - 2
23 | -
24 | exclude:
25 | - breakpoint
26 | - mq
27 |
28 | no-warn: 1
29 | no-debug: 1
30 | no-ids: 2
31 | no-important: 2
32 | hex-notation:
33 | - 2
34 | -
35 | style: uppercase
36 | indentation:
37 | - 2
38 | -
39 | size: 2
40 | property-sort-order:
41 | - 1
42 | -
43 | order: 'alphabetical'
44 | ignore-custom-properties: true
45 | variable-for-property:
46 | - 2
47 | -
48 | properties:
49 | - margin
50 | - content
--------------------------------------------------------------------------------
/src/scripts/pages/app.jsx:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense } from 'react';
2 | import { Route } from 'react-router-dom';
3 | import HomeContainer from '../container/homeContainer';
4 | import '../../scss/app.scss';
5 |
6 | const DetailCharacterLazy = lazy(() => import('../container/detailCharacterContainer'));
7 | const DetailComicLazy = lazy(() => import('../container/detailComicContainer'));
8 | const AboutLazy = lazy(() => import('./about'));
9 |
10 | class App extends React.Component {
11 | render() {
12 | return (
13 |
14 | loading...
}>
15 |
16 | } />
17 | } />
18 | } />
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/workbox-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | swDest: 'public/sw.js',
3 | runtimeCaching: [{
4 | // Match any same-origin request that contains 'api'.
5 | urlPattern: new RegExp(/https\:\/\/gateway\.marvel\.com\/v1\/public\/.+/g),
6 | handler: 'cacheFirst',
7 | options: {
8 | // Use a custom cache name for this route.
9 | cacheName: 'api-cache',
10 | // Configure custom cache expiration.
11 | expiration: {
12 | maxEntries: 20,
13 | maxAgeSeconds: 36000,
14 | },
15 | // Configure which responses are considered cacheable.
16 | cacheableResponse: {
17 | statuses: [0, 200]
18 | },
19 | },
20 | },
21 | {
22 | urlPattern: new RegExp(/https\:\/\/gateway\.marvel\.com\/v1\/public\/.+/g),
23 | handler: 'staleWhileRevalidate',
24 | options: {
25 | cacheableResponse: {
26 | statuses: [0, 200]
27 | }
28 | }
29 | }],
30 | clientsClaim: true,
31 | skipWaiting: true,
32 | globPatterns: ['**/*.{js,html,css,woff2,woff,svg}'],
33 | globFollow: false,
34 | globDirectory: './public/'
35 | }
36 |
--------------------------------------------------------------------------------
/src/scss/pages/about.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | @import '../base/easing';
3 |
4 | .about {
5 | background-color: color(invert);
6 | color: color(darkest);
7 | height: 100vh;
8 | padding: 0 20px;
9 | position: fixed;
10 | top: 0;
11 | transform: translate3d(0, 0, 0) translateY(100%) translate3d(0, 0, 0);
12 | transition: transform .25s easing(easeOutCirc);
13 | width: 100vw;
14 | z-index: 12;
15 |
16 | .close {
17 | cursor: pointer;
18 | display: inline-block;
19 | margin: 20px 0;
20 | position: relative;
21 | }
22 |
23 | .content {
24 | display: block;
25 | left: 50%;
26 | position: absolute;
27 | top: 50%;
28 | transform: translate(-50%, -50%);
29 | width: 80%;
30 | }
31 |
32 | &.animate {
33 | transform: translateY(0) translate3d(0, 0, 0);
34 | }
35 |
36 | .link {
37 | color: color(darkest);
38 | font-weight: bold;
39 | }
40 |
41 | .github {
42 | display: block;
43 | text-align: center;
44 |
45 | svg {
46 | height: 30px;
47 | width: 30px;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Ion Drimba F.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Page Not Found
6 |
7 |
52 |
53 |
54 | Page Not Found
55 | Sorry, but the page you were trying to view does not exist.
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :mortar_board: Marvel API Explorer
2 |
3 | 
4 | 
5 | [](https://coveralls.io/github/iondrimba/marvel-api-explorer)
6 |
7 | 
8 |
9 | ## About
10 |
11 | This is a personal project built in my spare time for learning purposes.
12 | It uses the official [Marvel API](https://developer.marvel.com/docs)
13 |
14 | ## Features
15 |
16 | * PWA
17 | * Responsive
18 | * Offline ready
19 | * Installable (add to Homescreen )
20 | * Swipe gestures for pagination
21 | * Pull to refresh keeps current pagination page
22 |
23 | ## Running
24 |
25 | ```bash
26 | npm start
27 | ```
28 |
29 | ## Testing
30 |
31 | ```bash
32 | npm test
33 | ```
34 |
35 | or
36 |
37 | ```bash
38 | npm run start:watch ## watch/tdd mode
39 | ```
40 |
41 | ## Built with
42 |
43 | * ES6
44 | * Sass
45 | * React + Redux
46 | * Jest
47 | * VSCode
48 | * WebPack
49 |
50 | ## Todo
51 |
52 | * Write more tests(especially jsx components)
53 | * Refactor bits of code
54 | * Add more mobile friendly gestures
55 |
--------------------------------------------------------------------------------
/src/scss/components/search.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | @import '../base/easing';
3 |
4 | .search {
5 | box-shadow: 0 2px 20px 0 color(shadow);
6 | position: fixed;
7 | top: 0;
8 | transform: translateY(0) translate3d(0, 0, 0);
9 | transition: transform .3s easing(easeOutExpo);
10 | width: 100%;
11 | z-index: 7;
12 |
13 | .search-wrapper {
14 | position: relative;
15 | width: 100%;
16 | }
17 |
18 | &.display {
19 | transform: translateY(100%) translate3d(0, 0, 0);
20 | transition: transform .3s easing(easeOutExpo);
21 | }
22 |
23 | form {
24 | display: flex;
25 | width: 100%;
26 |
27 | label {
28 | background-color: #757575;
29 | visibility: collapse;
30 | height: 0;
31 | width: 0;
32 | display: block;
33 | }
34 | }
35 |
36 | .close-icon {
37 | background-color: color(secondary);
38 | cursor: pointer;
39 | padding: 13px;
40 | position: absolute;
41 | right: 0;
42 | top: 0;
43 | transform: translateX(48px) translate3d(0, 0, 0);
44 | transition: transform .3s easing(easeOutExpo);
45 | width: 48px;
46 |
47 | &.show {
48 | transform: translateX(0) translate3d(0, 0, 0);
49 | }
50 | }
51 |
52 | input {
53 | border: 0;
54 | padding: 15px;
55 | width: 100%;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/scss/components/scroll-indicator.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | @import '../base/easing';
3 | $scroll-indicator-size: 24px;
4 | $scroll-indicator-icon-size: 10px;
5 |
6 | .scroll-indicator {
7 | align-items: center;
8 | background-color: transparent;
9 | border: 0;
10 | cursor: pointer;
11 | display: flex;
12 | height: $scroll-indicator-size;
13 | justify-content: center;
14 | left: 50%;
15 | opacity: 1;
16 | position: fixed;
17 | top: -30px;
18 | transform: translateX(-50%);
19 | width: $scroll-indicator-size;
20 | z-index: 2;
21 |
22 | &__icon {
23 | animation: yoyo-scroll .8s infinite;
24 | border-bottom: 0;
25 | border-left: 2px solid color(invert);
26 | border-right: 0;
27 | border-top: 2px solid color(invert);
28 | display: block;
29 | height: $scroll-indicator-icon-size;
30 | margin: 0;
31 | padding: 0;
32 | position: absolute;
33 | transform: translateY(0) translate3d(0, 0, 0) rotate(45deg);
34 | width: $scroll-indicator-icon-size;
35 | }
36 | }
37 |
38 | @keyframes yoyo-scroll {
39 | from {
40 | transform: translateY(0) translate3d(0, 0, 0) rotate(45deg);
41 | }
42 |
43 | 50% {
44 | transform: translateY(-50%) translate3d(0, 0, 0) rotate(45deg);
45 | }
46 |
47 | to {
48 | transform: translateY(0) translate3d(0, 0, 0) rotate(45deg);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/scripts/model/api.js:
--------------------------------------------------------------------------------
1 | import apiKeys from '../../../api.keys';
2 | import axios from 'axios';
3 |
4 | class Api {
5 | constructor(API_KEY, HASH) {
6 | this.publicKey = API_KEY;
7 | this.hash = HASH;
8 | this.timeout = 10000;
9 | this.options = {
10 | limit: 15,
11 | offset: 0
12 | };
13 |
14 | this.instance = axios.create({
15 | baseURL: `${apiKeys.baseUrl}/${apiKeys.version}/${apiKeys.folder}/`,
16 | timeout: this.timeout
17 | });
18 | }
19 |
20 | appendParameters(url, options) {
21 | let { page, orderBy, titleStartsWith, nameStartsWith } = options;
22 | let fetchUrl = `${url}?ts=1&apikey=${this.publicKey}&hash=${this.hash}`;
23 | console.log(`Appending parameters to URL: ${fetchUrl}`);
24 |
25 | this.options.offset = 0;
26 |
27 | if (page > 0) {
28 | page--;
29 | this.options.offset = page * this.options.limit;
30 | }
31 |
32 | let mergedOptions = Object.assign({}, { orderBy }, this.options);
33 |
34 | mergedOptions = titleStartsWith ? Object.assign({}, { titleStartsWith }, mergedOptions) : mergedOptions;
35 | mergedOptions = nameStartsWith ? Object.assign({}, { nameStartsWith }, mergedOptions) : mergedOptions;
36 |
37 | for (let option in mergedOptions) {
38 | fetchUrl += `&${option}=${mergedOptions[option]}`;
39 | }
40 |
41 | return fetchUrl;
42 | }
43 |
44 | get(options) {
45 | return this.instance.get(this.appendParameters(options.url, options));
46 | }
47 | }
48 |
49 | export default Api;
50 |
--------------------------------------------------------------------------------
/src/scripts/pages/detailsCommon.js:
--------------------------------------------------------------------------------
1 | import Infos from '../components/infos'
2 | import React from 'react';
3 |
4 | export const animateIn = (slides) => {
5 | slides.map((el, index) => {
6 | requestAnimationFrame(() => {
7 | requestAnimationFrame(() => {
8 | el.classList.add('active');
9 | });
10 | });
11 | });
12 |
13 | const loader = document.querySelector('.slides .loader');
14 |
15 | setTimeout(() => {
16 | loader.classList.add('show');
17 | }, 200);
18 | }
19 |
20 | export const enableScroll = () => {
21 | document.querySelector('html').classList.remove('disable-scroll');
22 | }
23 |
24 | export const disableScroll = () => {
25 | document.querySelector('html').classList.add('disable-scroll');
26 | }
27 |
28 | export const coverOnLoad = (content, img, slides, positionInfos, tilt) => {
29 | setTimeout(() => {
30 | const loader = document.querySelector('.slides .loader');
31 |
32 | setTimeout(() => {
33 | positionInfos();
34 |
35 | content.classList.add('active');
36 | img.classList.add('show');
37 |
38 | slides.reverse().map((el, index) => {
39 | loader.classList.remove('show');
40 | el.classList.remove('active');
41 | el.classList.add('out');
42 | });
43 |
44 | tilt.init(img);
45 | }, 800);
46 | }, 300);
47 |
48 | disableScroll();
49 | }
50 |
51 | export const infoData = (data, hasItens, title)=> {
52 | return hasItens(data) ? : ''
53 | }
54 |
--------------------------------------------------------------------------------
/src/scss/components/header.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 |
3 | .header {
4 | background-color: color(header);
5 | box-shadow: 0 2px 20px 0 rgba(0, 0, 0, .75);
6 | display: flex;
7 | flex-direction: row;
8 | height: 50px;
9 | justify-content: space-between;
10 | position: fixed;
11 | top: 0;
12 | width: 100%;
13 | z-index: 9;
14 |
15 | .logo {
16 | vertical-align: middle;
17 | width: 50px;
18 | }
19 |
20 | &__dots {
21 | background-color: transparent;
22 | border: 0;
23 | display: flex;
24 | justify-content: center;
25 | margin: 0;
26 | padding: 0;
27 | width: 50px;
28 |
29 | span {
30 | display: none;
31 | }
32 | }
33 |
34 | &__search {
35 | background-color: transparent;
36 | border: 0;
37 | margin: 0;
38 | padding: 0;
39 | position: relative;
40 | width: 50px;
41 |
42 | .close-icon {
43 | left: 20px;
44 | }
45 |
46 | .search-icon, .close-icon {
47 | cursor: pointer;
48 | height: 40px;
49 | left: 50%;
50 | opacity: 1;
51 | position: absolute;
52 | top: 50%;
53 | transform: translate(-50%, -50%) translate3d(0, 0, 0);
54 | transition: opacity .2s ;
55 | width: 25px;
56 |
57 | &.hide {
58 | opacity: 0;
59 | }
60 | }
61 | }
62 |
63 | h1 {
64 | align-self: center;
65 | position: relative;
66 |
67 | span {
68 | margin-left: 4px;
69 | position: relative;
70 | top: 3px;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/scss/components/slides.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | @import '../base/easing';
3 |
4 |
5 | .slides {
6 | display: block;
7 | height: 100vh;
8 | pointer-events: none;
9 | position: fixed;
10 | width: 100vw;
11 | z-index: 9;
12 |
13 | .loader {
14 | animation: yoyo .8s infinite;
15 | opacity: 0;
16 | transition: opacity .3s;
17 | width: 35%;
18 | z-index: 1;
19 |
20 | &.show {
21 | opacity: 1;
22 | }
23 | }
24 |
25 | @keyframes yoyo {
26 | from {
27 | transform: translate(-50%, -50%) translate3d(0, 0, 0);
28 | }
29 |
30 | 50% {
31 | transform: translate(-50%, -50%) scale(1.2) translate3d(0, 0, 0);
32 | }
33 |
34 | to {
35 | transform: translate(-50%, -50%) translate3d(0, 0, 0);
36 | }
37 | }
38 |
39 | .first, .second {
40 | display: block;
41 | height: 200vh;
42 | position: absolute;
43 | transform: scaleY(0) translate3d(0, 0, 0);
44 | transform-origin: top;
45 | width: 200vh;
46 |
47 | &.active {
48 | transform: scaleY(1) translate3d(0, 0, 0);
49 | }
50 |
51 | &.out {
52 | transform: scaleY(0) translate3d(0, 0, 0);
53 | transform-origin: bottom;
54 | transition: transform 2s .5s easing(easeOutExpo);
55 | }
56 | }
57 |
58 | .first {
59 | background-color: color(primary);
60 | transition: transform 2s easing(easeOutExpo);
61 | }
62 |
63 | .second {
64 | background-color: color(primary);
65 | transition: transform 2s .5s easing(easeOutExpo);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/scss/components/pagination.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 |
3 | .pagination {
4 | background-color: color(pagination);
5 | bottom: 0;
6 | box-shadow: 0 -4px 20px 0 color(shadow);
7 | display: flex;
8 | left: 0;
9 | position: fixed;
10 | width: 100%;
11 | z-index: 2;
12 |
13 | &__content {
14 | display: flex;
15 | height: 50px;
16 | justify-content: center;
17 | width: 100%;
18 | }
19 |
20 | &__link {
21 | align-items: center;
22 | color: color(invert);
23 | cursor: pointer;
24 | display: flex;
25 | justify-content: center;
26 | padding: 10px 15px;
27 | position: relative;
28 | text-decoration: none;
29 | transition: background-color .2s;
30 | width: auto;
31 | z-index: 1;
32 |
33 | &--active {
34 | background-color: color(pagination-selected);
35 | box-shadow: 0 1px 15px 0 color(shadow);
36 | z-index: 2;
37 | }
38 | }
39 |
40 | &__prev, &__next {
41 | display: block;
42 | height: 10px;
43 | margin: 0;
44 | padding: 0;
45 | position: absolute;
46 | transform: rotate(-45deg);
47 | width: 10px;
48 | text-indent: -9999px;
49 | overflow: hidden;
50 | }
51 |
52 | &__prev {
53 | border-bottom: 0;
54 | border-left: 2px solid color(invert);
55 | border-right: 0;
56 | border-top: 2px solid color(invert);
57 |
58 | }
59 |
60 | &__next {
61 | border-bottom: 2px solid color(invert);
62 | border-left: 0;
63 | border-right: 2px solid color(invert);
64 | border-top: 0;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/scripts/components/menu.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Menu extends PureComponent {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | filter: this.props.filter
9 | };
10 | }
11 |
12 | onClick(evt) {
13 | const filter = evt.currentTarget.innerText.toLowerCase();
14 | this.setState({ filer: filter });
15 | this.props.filterAction(filter, this.props);
16 | this.props.onClick();
17 | this.props.toogleMenuAction(false);
18 | }
19 | onSelect(evt) {
20 | this.props.toogleMenuAction(!this.props.menuOpen);
21 | }
22 |
23 | toogleVisibility() {
24 | return this.props.menuOpen ? 'show' : 'hide';
25 | }
26 |
27 | render() {
28 | return (
29 | this.menu = c} className={`menu ${this.props.menuOpen ? '' : 'show'}`}>
30 | {this.props.filter}
31 | this.list = c} className={`list ${this.toogleVisibility()}`}>
32 | Characters
33 | Comics
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | Menu.propTypes = {
41 | filter: PropTypes.string,
42 | toogleMenuAction: PropTypes.func,
43 | onClick: PropTypes.func,
44 | menuOpen: PropTypes.bool,
45 | filterAction: PropTypes.func
46 | }
47 |
48 | export default Menu;
49 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: build
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | types: [opened, synchronize, reopened, ready_for_review]
11 | branches: [ main ]
12 |
13 | jobs:
14 | build:
15 | name: build
16 | runs-on: ubuntu-latest
17 | strategy:
18 | matrix:
19 | node-version: [15.11.0]
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | env:
28 | CI: true
29 | PUBLIC_API_KEY: ${{ secrets.PUBLIC_API_KEY }}
30 | - run: npm ci
31 | - run: npm set apikey=$PUBLIC_API_KEY && npm run prod && npm run workbox
32 |
33 | test:
34 | name: test
35 | runs-on: ubuntu-latest
36 | strategy:
37 | matrix:
38 | node-version: [15.11.0]
39 |
40 | steps:
41 | - uses: actions/checkout@main
42 | - name: Use Node.js ${{ matrix.node-version }}
43 | uses: actions/setup-node@main
44 | with:
45 | node-version: ${{ matrix.node-version }}
46 | - run: npm ci
47 | - uses: paambaati/codeclimate-action@v2.6.0
48 | env:
49 | CI: true
50 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
51 | with:
52 | coverageCommand: npm test
53 |
--------------------------------------------------------------------------------
/src/scripts/pages/about.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import CloseIcon from '../components/close-icon';
3 | import PropTypes from 'prop-types';
4 |
5 | class About extends PureComponent {
6 | componentDidMount() {
7 | setTimeout(() => {
8 | requestAnimationFrame(() => {
9 | this.about.classList.add('animate');
10 | })
11 | }, 50);
12 | }
13 |
14 | render() {
15 | return (
16 | this.about = c} className="about" >
17 |
18 |
19 |
20 |
21 |
About
22 |
23 | MARVEL API Explorer is a personal project made by Ion Drimba Filho using ReactJS + Redux and the oficial Marvel API .
24 | It also works as a PWA.
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | About.propTypes = {
36 | 'history': PropTypes.object
37 | };
38 |
39 | export default About;
40 |
--------------------------------------------------------------------------------
/public/app.1d43c609c894bd6b91dc.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*! Hammer.JS - v2.0.7 - 2016-04-22
8 | * http://hammerjs.github.io/
9 | *
10 | * Copyright (c) 2016 Jorik Tangelder;
11 | * Licensed under the MIT license */
12 |
13 | /** @license React v0.20.2
14 | * scheduler.production.min.js
15 | *
16 | * Copyright (c) Facebook, Inc. and its affiliates.
17 | *
18 | * This source code is licensed under the MIT license found in the
19 | * LICENSE file in the root directory of this source tree.
20 | */
21 |
22 | /** @license React v16.13.1
23 | * react-is.production.min.js
24 | *
25 | * Copyright (c) Facebook, Inc. and its affiliates.
26 | *
27 | * This source code is licensed under the MIT license found in the
28 | * LICENSE file in the root directory of this source tree.
29 | */
30 |
31 | /** @license React v17.0.2
32 | * react-dom.production.min.js
33 | *
34 | * Copyright (c) Facebook, Inc. and its affiliates.
35 | *
36 | * This source code is licensed under the MIT license found in the
37 | * LICENSE file in the root directory of this source tree.
38 | */
39 |
40 | /** @license React v17.0.2
41 | * react-is.production.min.js
42 | *
43 | * Copyright (c) Facebook, Inc. and its affiliates.
44 | *
45 | * This source code is licensed under the MIT license found in the
46 | * LICENSE file in the root directory of this source tree.
47 | */
48 |
49 | /** @license React v17.0.2
50 | * react.production.min.js
51 | *
52 | * Copyright (c) Facebook, Inc. and its affiliates.
53 | *
54 | * This source code is licensed under the MIT license found in the
55 | * LICENSE file in the root directory of this source tree.
56 | */
57 |
--------------------------------------------------------------------------------
/src/scripts/components/header.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Search from './search';
3 | import { Link } from 'react-router-dom';
4 | import PropTypes from 'prop-types';
5 |
6 | class Header extends PureComponent {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | displaySearch: false
11 | }
12 | }
13 | onSearchClick() {
14 | this.setState({ displaySearch: !this.state.displaySearch });
15 | this.props.toogleMenuAction(false);
16 | }
17 | onSearch() {
18 | this.setState({ displaySearch: false });
19 | }
20 | render() {
21 | return (
22 |
23 |
24 |
25 | about
26 |
27 |
28 | API Explorer
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | Header.propTypes = {
41 | toogleMenuAction: PropTypes.func
42 | }
43 |
44 | export default Header;
45 |
--------------------------------------------------------------------------------
/public/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Marvel API",
3 | "short_name": "Marvel API",
4 | "theme_color": "#bd1023",
5 | "background_color": "#bd1023",
6 | "display": "standalone",
7 | "orientation": "portrait",
8 | "Scope": "/",
9 | "start_url": "/",
10 | "icons": [
11 | {
12 | "src": "/images/icons/icon-72x72.png",
13 | "sizes": "72x72",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "/images/icons/icon-96x96.png",
18 | "sizes": "96x96",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "/images/icons/icon-128x128.png",
23 | "sizes": "128x128",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "/images/icons/icon-144x144.png",
28 | "sizes": "144x144",
29 | "type": "image/png"
30 | },
31 | {
32 | "src": "/images/icons/icon-152x152.png",
33 | "sizes": "152x152",
34 | "type": "image/png"
35 | },
36 | {
37 | "src": "/images/icons/icon-192x192.png",
38 | "sizes": "192x192",
39 | "type": "image/png"
40 | },
41 | {
42 | "src": "/images/icons/icon-384x384.png",
43 | "sizes": "384x384",
44 | "type": "image/png"
45 | },
46 | {
47 | "src": "/images/icons/icon-512x512.png",
48 | "sizes": "512x512",
49 | "type": "image/png"
50 | },
51 | {
52 | "src": "/images/android-chrome-192x192.png?v=PY47E7xJdP",
53 | "sizes": "192x192",
54 | "type": "image/png"
55 | },
56 | {
57 | "src": "/images/android-chrome-512x512.png?v=PY47E7xJdP",
58 | "sizes": "512x512",
59 | "type": "image/png"
60 | }
61 | ],
62 | "splash_pages": null
63 | }
64 |
--------------------------------------------------------------------------------
/src/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Marvel API",
3 | "short_name": "Marvel API",
4 | "theme_color": "#bd1023",
5 | "background_color": "#bd1023",
6 | "display": "standalone",
7 | "orientation": "portrait",
8 | "Scope": "/",
9 | "start_url": "/",
10 | "icons": [
11 | {
12 | "src": "/images/icons/icon-72x72.png",
13 | "sizes": "72x72",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "/images/icons/icon-96x96.png",
18 | "sizes": "96x96",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "/images/icons/icon-128x128.png",
23 | "sizes": "128x128",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "/images/icons/icon-144x144.png",
28 | "sizes": "144x144",
29 | "type": "image/png"
30 | },
31 | {
32 | "src": "/images/icons/icon-152x152.png",
33 | "sizes": "152x152",
34 | "type": "image/png"
35 | },
36 | {
37 | "src": "/images/icons/icon-192x192.png",
38 | "sizes": "192x192",
39 | "type": "image/png"
40 | },
41 | {
42 | "src": "/images/icons/icon-384x384.png",
43 | "sizes": "384x384",
44 | "type": "image/png"
45 | },
46 | {
47 | "src": "/images/icons/icon-512x512.png",
48 | "sizes": "512x512",
49 | "type": "image/png"
50 | },
51 | {
52 | "src": "/images/android-chrome-192x192.png?v=PY47E7xJdP",
53 | "sizes": "192x192",
54 | "type": "image/png"
55 | },
56 | {
57 | "src": "/images/android-chrome-512x512.png?v=PY47E7xJdP",
58 | "sizes": "512x512",
59 | "type": "image/png"
60 | }
61 | ],
62 | "splash_pages": null
63 | }
64 |
--------------------------------------------------------------------------------
/src/scripts/misc/tilt.js:
--------------------------------------------------------------------------------
1 | class Tilt {
2 | constructor() {
3 | this.offset = 0.1;
4 | this.animation = 300;
5 | }
6 |
7 | init(el) {
8 | this.el = el;
9 | this.rect = this.el.getBoundingClientRect();
10 | this.over = false;
11 | this.timer;
12 | this._addListeners();
13 | }
14 |
15 | _addListeners() {
16 | this.el.addEventListener('mouseover', this._onMouseOver.bind(this));
17 | this.el.addEventListener('mousemove', this._onMouseMove.bind(this));
18 | this.el.addEventListener('mouseout', this._onMouseOut.bind(this));
19 | }
20 |
21 | _onMouseOver(evt) {
22 | if (this.over) {
23 | this._position(evt, evt.currentTarget);
24 | }
25 | }
26 |
27 | _onMouseMove(evt) {
28 | this._position(evt, evt.currentTarget);
29 | this._transition(evt.currentTarget);
30 | }
31 |
32 | _onMouseOut(evt) {
33 | this._applyStyle(evt.currentTarget, {
34 | transform: 'rotateX(0deg) rotateY(0deg) translate(-50%, -50%)'
35 | });
36 | this.over = false;
37 | }
38 |
39 | _convertToString(obj) {
40 | let output = '';
41 | for (var prop in obj) {
42 | output += `${prop}:${obj[prop]};`;
43 | }
44 | return output;
45 | }
46 |
47 | _applyStyle(el, props) {
48 | el.style = this._convertToString(props);
49 | }
50 |
51 | _position(evt, el) {
52 | const rect = this.rect;
53 | const o = this.offset;
54 |
55 | const v = {
56 | x: -(evt.offsetX - (rect.width * .5)) * o,
57 | y: (evt.offsetY - (rect.height * .5)) * .03
58 | };
59 |
60 | this._applyStyle(el, { transform: 'rotateX(' + v.y + 'deg) rotateY(' + v.x + 'deg) translate(-50%, -50%)' });
61 | }
62 |
63 | _transition() {
64 | if (this.timer !== undefined) {
65 | clearTimeout(this.timer)
66 | }
67 |
68 | this.timer = setTimeout(() => {
69 | this.over = true;
70 | }, this.animation);
71 | }
72 | }
73 |
74 | export default Tilt;
75 |
--------------------------------------------------------------------------------
/src/scripts/model/paginationHelper.js:
--------------------------------------------------------------------------------
1 | class PaginationHelper {
2 | constructor() {
3 | this.maxPages = 5;
4 | }
5 |
6 | mountGroups(totalItens) {
7 | let count = 0;
8 | let groups = [];
9 | let pages = 0;
10 |
11 | const total = this.getTotalPages(totalItens, this.maxPages);
12 |
13 | for (let i = 0; i < total; i++) {
14 | groups.push([]);
15 |
16 | pages = this.maxPages;
17 |
18 | if (totalItens < this.maxPages) {
19 | pages = totalItens;
20 | } else if (i === Math.floor(total)) {
21 | pages = totalItens % count;
22 | }
23 |
24 | for (let j = 0; j < pages; j++) {
25 | groups[i].push(count);
26 | count++;
27 | }
28 | }
29 | return groups;
30 | }
31 |
32 | getCurrentGroup(groups, currentPage) {
33 | return groups[currentPage] || [];
34 | }
35 |
36 | groupPages(currentPage = 1) {
37 | let start = Number(currentPage) / this.maxPages;
38 | return Math.floor(start);
39 | }
40 |
41 | getPages(pagination) {
42 | const pages = this.getCurrentGroup(
43 | this.mountGroups(pagination.total),
44 | this.groupPages(pagination.current - 1)
45 | );
46 |
47 | return pages <= 1 ? [] : pages;
48 | }
49 |
50 | getTotalPages(totalItens, maxPages) {
51 | return totalItens / maxPages;
52 | }
53 |
54 | hasNext(pagination) {
55 | return (
56 | pagination.total > 1 &&
57 | (pagination.total > 1 && pagination.current < pagination.total)
58 | );
59 | }
60 |
61 | hasPrev(pagination) {
62 | return pagination.total > 0 && pagination.current > 1;
63 | }
64 |
65 | getPrev(pagination) {
66 | if (this.hasPrev(pagination)) {
67 | return --pagination.current;
68 | }
69 | }
70 |
71 | getNext(pagination) {
72 | if (this.hasNext(pagination)) {
73 | return ++pagination.current;
74 | }
75 | }
76 | }
77 |
78 | export default PaginationHelper;
79 |
--------------------------------------------------------------------------------
/src/scripts/components/grid.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import GridItem from './grid-item';
3 | import PropTypes from 'prop-types';
4 |
5 | class Grid extends PureComponent {
6 | componentDidUpdate(prevProps, prevState) {
7 | this.animate([...document.querySelectorAll('.grid .thumb')]);
8 | }
9 |
10 | animate(imgs) {
11 | imgs.map((el, index) => {
12 | setTimeout((elm) => {
13 | requestAnimationFrame(() => {
14 | elm.classList.remove('fetching');
15 | elm.classList.add('fetched');
16 | });
17 |
18 | let image = new Image();
19 | const file = elm.querySelector('.thumb__file');
20 | const mask = elm.querySelector('.thumb__mask');
21 | const title = elm.querySelector('.thumb__title');
22 |
23 | image.onload = function () {
24 | mask.classList.add('animate');
25 | file.attributes.src.value = '';
26 | file.attributes.src.value = image.src;
27 | setTimeout((el) => {
28 | requestAnimationFrame(() => {
29 | file.classList.add('animate');
30 | mask.classList.add('animate-out');
31 |
32 | if (image.src.indexOf('missing') > -1) {
33 | title.classList.add('show');
34 | }
35 |
36 | });
37 | }, 300, elm);
38 | };
39 | image.src = file.attributes['data-src'].value;
40 | }, index * 50, el);
41 | });
42 | }
43 |
44 | render() {
45 | return (
46 |
47 | {
48 | this.props.data.map((data, index) => {
49 | return
50 | })
51 | }
52 |
53 | );
54 | }
55 | }
56 |
57 | Grid.propTypes = {
58 | data: PropTypes.array,
59 | filter: PropTypes.string
60 | }
61 |
62 | export default Grid;
63 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [main]
9 | schedule:
10 | - cron: '0 3 * * 0'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | # Override automatic language detection by changing the below list
21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
22 | language: ['javascript']
23 | # Learn more...
24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v2
29 | with:
30 | # We must fetch at least the immediate parents so that if this is
31 | # a pull request then we can checkout the head.
32 | fetch-depth: 2
33 |
34 | # If this run was triggered by a pull request event, then checkout
35 | # the head of the pull request instead of the merge commit.
36 | - run: git checkout HEAD^2
37 | if: ${{ github.event_name == 'pull_request' }}
38 |
39 | # Initializes the CodeQL tools for scanning.
40 | - name: Initialize CodeQL
41 | uses: github/codeql-action/init@v2
42 | with:
43 | languages: ${{ matrix.language }}
44 |
45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
46 | # If this step fails, then you should remove it and run the build manually (see below)
47 | - name: Autobuild
48 | uses: github/codeql-action/autobuild@v2
49 |
50 | # ℹ️ Command-line programs to run using the OS shell.
51 | # 📚 https://git.io/JvXDl
52 |
53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
54 | # and modify them (or add more) to build your code if your project
55 | # uses a compiled language
56 |
57 | #- run: |
58 | # make bootstrap
59 | # make release
60 |
61 | - name: Perform CodeQL Analysis
62 | uses: github/codeql-action/analyze@v2
63 |
--------------------------------------------------------------------------------
/src/scss/components/thumb.scss:
--------------------------------------------------------------------------------
1 | .thumb {
2 | background-image: url('../images/missing.jpg');
3 | background-size: cover;
4 | border-radius: 4px;
5 | margin-bottom: 2.6vw;
6 | margin-right: 2.6vw;
7 | opacity: 0;
8 | overflow: hidden;
9 | position: relative;
10 | transform: translateY(100px) ;
11 | transition: opacity .2s, transform .3s easing(easeOutExpo);
12 | width: 31.45%;
13 |
14 | &__file {
15 | float: left;
16 | opacity: 0;
17 | width: 100%;
18 |
19 | &.animate {
20 | opacity: 1;
21 | }
22 | }
23 |
24 | &__title {
25 | align-items: center;
26 | background-color: #171515;
27 | color: color(invert);
28 | display: inline-flex;
29 | font-size: 14px;
30 | font-weight: bold;
31 | justify-content: center;
32 | left: 0;
33 | min-height: 80px;
34 | opacity: 0;
35 | padding: 10px;
36 | position: absolute;
37 | text-align: center;
38 | text-decoration: none;
39 | top: 50%;
40 | transform: translateY(-50%);
41 | width: 100%;
42 | z-index: 1;
43 |
44 | &.show {
45 | opacity: 1;
46 | transition: opacity .2s .6s;
47 | }
48 | }
49 |
50 | &__mask {
51 | background-color: color(mask);
52 | display: block;
53 | height: 100%;
54 | position: absolute;
55 | transform: scaleY(0) translate3d(0, 0, 0);
56 | transform-origin: top;
57 | transition: transform .3s .1s easing(easeOutExpo);
58 | width: 100%;
59 | z-index: 1;
60 |
61 | &.animate {
62 | transform: scaleY(1) translate3d(0, 0, 0);
63 | }
64 |
65 | &.animate-out {
66 | transform: scaleY(0) translate3d(0, 0, 0);
67 | transform-origin: bottom;
68 | }
69 | }
70 |
71 | &.fetching {
72 | opacity: 0;
73 | transform: translateY(100px);
74 | }
75 |
76 | &.fetched {
77 | opacity: 1;
78 | transform: translateY(0);
79 | }
80 |
81 | &::before {
82 | background-image: linear-gradient(160deg, rgba(255, 255, 255, .15) 50%, transparent 50%);
83 | content: '';
84 | height: 100%;
85 | left: 0;
86 | position: absolute;
87 | width: 100%;
88 | z-index: 2;
89 | }
90 |
91 | &:nth-child(3n + 0) {
92 | margin-right: 0;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/scripts/components/search.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Menu from './menu';
3 | import PropTypes from 'prop-types';
4 |
5 | class Search extends PureComponent {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | search: props.search
10 | }
11 | }
12 |
13 | componentDidMount() {
14 | this.form.onsubmit = (evt) => {
15 | evt.preventDefault();
16 | this.props.searchAction(this.state.search, this.props);
17 | this.search.classList.add('hide');
18 | this.props.onSearch();
19 | this.form.blur();
20 | this.searchInput.blur();
21 | }
22 | }
23 |
24 | componentDidUpdate() {
25 | if (this.props.display) {
26 | this.searchInput.focus();
27 | }
28 | }
29 |
30 | onTextChange(evt) {
31 | const search = evt.currentTarget.value;
32 | this.setState({ search: search });
33 | }
34 |
35 | onClick() {
36 | this.props.onSearch();
37 | }
38 |
39 | onSearchClear() {
40 | this.props.searchClear('');
41 | this.setState({ search: this.props.search });
42 | }
43 |
44 | render() {
45 | return (
46 | this.search = c} className={this.props.display ? 'search display' : 'search'}>
47 |
56 |
57 | );
58 | }
59 | }
60 |
61 | Search.propTypes = {
62 | searchAction: PropTypes.func,
63 | searchClear: PropTypes.func,
64 | onSearch: PropTypes.func,
65 | search: PropTypes.string,
66 | display: PropTypes.bool
67 | }
68 |
69 | export default Search;
70 |
--------------------------------------------------------------------------------
/src/scripts/components/pagination.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PaginationLink from './pagination-link';
3 | import PropTypes from 'prop-types';
4 |
5 | class Pagination extends PureComponent {
6 | getQueryString(search) {
7 | return search ? `?search=${search}` : '';
8 | }
9 |
10 | onClick(evt) {
11 | evt.preventDefault();
12 | this.props.paginationAction(evt.currentTarget.attributes.href.value, this.props);
13 | }
14 |
15 | getUrl(page) {
16 | return `/${this.props.filter}/${page}${this.getQueryString(this.props.search)}`;
17 | }
18 |
19 | getStyle(props, page) {
20 | return Number(props.pagination.current) === page ? 'pagination__link pagination__link--active' : 'pagination__link';
21 | }
22 |
23 | previous() {
24 | this.props.paginationPrevAction(this.props);
25 | }
26 |
27 | next() {
28 | this.props.paginationNextAction(this.props);
29 | }
30 |
31 | _paginationLink(delta, label, key) {
32 | return ;
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
45 | {this.props.pagination.prev ? this._paginationLink(-1, 'previous', 'prev') : null}
46 | {
47 | this.props.pagination.pages.map((data, index) => {
48 | return
49 | })
50 | }
51 | {this.props.pagination.next ? this._paginationLink(+1, 'next', 'next') : null}
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | Pagination.propTypes = {
59 | filter: PropTypes.string,
60 | pagination: PropTypes.object,
61 | search: PropTypes.string,
62 | paginationAction: PropTypes.func,
63 | paginationNextAction: PropTypes.func,
64 | paginationPrevAction: PropTypes.func
65 | }
66 |
67 | export default Pagination;
68 |
--------------------------------------------------------------------------------
/src/scripts/pages/home.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Loader from '../components/loader';
4 | import Grid from '../components/grid';
5 | import Pagination from '../components/pagination';
6 | import Error from '../components/error';
7 | import Header from '../components/header';
8 | import Hammer from 'hammerjs';
9 | import Explore from '../components/explore';
10 |
11 | class Home extends Component {
12 | componentDidMount() {
13 | const stage = document.getElementsByClassName('grid')[0];
14 | const mc = new Hammer.Manager(stage, {
15 | touchAction: 'pan-y'
16 | });
17 |
18 | const Swipe = new Hammer.Swipe();
19 |
20 | mc.add(Swipe);
21 | mc.on('swiperight', (e) => {
22 | this.pagination.previous();
23 | });
24 |
25 | mc.on('swipeleft', (e) => {
26 | this.pagination.next();
27 | });
28 | }
29 |
30 | onExploreClick() {
31 | this.props.firstFetch(this.props);
32 | }
33 |
34 | shouldComponentUpdate(nextProps, nextState) {
35 | return nextProps.location.pathname.indexOf('detail') === -1;
36 | }
37 |
38 | componentDidUpdate(prevProps, prevState) {
39 | window.scroll(0, 0);
40 |
41 | if ((prevProps.match.params.page !== this.props.match.params.page) && !isNaN(this.props.match.params.page) && !isNaN(prevProps.match.params.page)) {
42 | this.props.fetchAction(Number(this.props.match.params.page));
43 | }
44 | }
45 |
46 | render() {
47 | const { data, filter } = this.props;
48 |
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 |
this.pagination = c} {...this.props} />
57 |
58 | );
59 | }
60 | }
61 |
62 | Home.propTypes = {
63 | match: PropTypes.object,
64 | error: PropTypes.object,
65 | location: PropTypes.object,
66 | data: PropTypes.array,
67 | pagination: PropTypes.object,
68 | fetchAction: PropTypes.func,
69 | searchClear: PropTypes.func,
70 | searchAction: PropTypes.func,
71 | firstFetch: PropTypes.func,
72 | filter: PropTypes.string,
73 | fetching: PropTypes.bool
74 | }
75 |
76 | export default Home;
77 |
--------------------------------------------------------------------------------
/src/scripts/pages/detailCharacter.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Transition from '../components/transition';
4 | import BackButton from '../components/back-button';
5 | import Cover from '../components/cover';
6 | import ScrollIndicator from '../components/scroll-indicator';
7 | import Tilt from '../misc/tilt';
8 | import { animateIn, enableScroll, coverOnLoad, infoData } from './detailsCommon';
9 |
10 | class DetailCharacter extends PureComponent {
11 | constructor(props) {
12 | super(props);
13 | }
14 |
15 | positionInfos() {
16 | let viewH = 0;
17 | if (window.innerWidth < 960) {
18 | viewH = innerHeight - 50;
19 | }
20 |
21 | this.infos.style = `transform:translateY(${viewH}px)`;
22 | }
23 |
24 | componentDidMount() {
25 | this.tilt = new Tilt();
26 | window.onresize = () => { this.positionInfos(); };
27 |
28 | const content = document.querySelector('.detail__content');
29 | const img = content.querySelector('img');
30 | img.onload = () => { coverOnLoad(content, img, slides, this.positionInfos.bind(this), this.tilt); };
31 |
32 | let slides = [...document.querySelectorAll('.slides .first')];
33 | this.animateIn(slides);
34 | }
35 |
36 | componentWillUnmount() {
37 | window.onresize = null;
38 | enableScroll();
39 | }
40 |
41 | onBackButtonClick() {
42 | this.props.history.goBack();
43 | }
44 |
45 | animateIn(slides) {
46 | animateIn(slides);
47 | }
48 |
49 | hasItens(data) {
50 | return data.items && data.items.length;
51 | }
52 |
53 | render() {
54 | return (
55 |
56 |
57 |
this.content = c} className="detail__content">
58 |
59 |
60 |
this.infos = c} className="detail__infos">
61 |
62 | this.infoName = c} className="info__name">
63 |
{this.props.selectedItem.name}
64 |
65 | {infoData(this.props.selectedItem.stories, this.hasItens, 'Stories')}
66 | {infoData(this.props.selectedItem.series, this.hasItens, 'Series')}
67 |
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | DetailCharacter.propTypes = {
75 | selectedItem: PropTypes.object,
76 | history: PropTypes.object
77 | }
78 |
79 | export default DetailCharacter;
80 |
--------------------------------------------------------------------------------
/test/model/api.test.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import Api from '../../src/scripts/model/api';
3 |
4 | jest.mock('axios');
5 |
6 | axios.create.mockImplementationOnce(() => axios);
7 |
8 | describe('API tests', () => {
9 |
10 | const PUBLIC_API_KEY = process.env.PUBLIC_API_KEY || '######';
11 | const api = new Api(PUBLIC_API_KEY, '######');
12 |
13 | it('Api.getCharacters - should retrive character requests', async () => {
14 | const data = {
15 | response: {
16 | data: {
17 | code: 200
18 | }
19 | }
20 | };
21 |
22 | axios.get.mockImplementationOnce(() => Promise.resolve(data));
23 |
24 | await expect(api.get({ page: 1, orderBy: 'name', nameStartsWith: 'spi', url: '/characters' }))
25 | .resolves
26 | .toEqual(data);
27 | });
28 |
29 | it('Api.getCharacters - should retrive character with offset 15', async () => {
30 | const data = {
31 | response: {
32 | data: {
33 | code: 200,
34 | data: {
35 | offset: 15
36 | }
37 | }
38 | }
39 | };
40 |
41 | axios.get.mockImplementationOnce(() => Promise.resolve(data));
42 |
43 | await expect(api.get({ page: 2, orderBy: 'name', nameStartsWith: 'spi', url: '/characters' }))
44 | .resolves
45 | .toEqual(data);
46 | });
47 |
48 | it('Api.getCharacters - should retrive character by name that starts with spi', async () => {
49 | const data = {
50 | response: {
51 | data: {
52 | code: 200,
53 | data: {
54 | results: [
55 | { name: 'spider-dok' }
56 | ]
57 | }
58 | }
59 | }
60 | };
61 |
62 | axios.get.mockImplementationOnce(() => Promise.resolve(data));
63 |
64 | await expect(api.get({ page: 1, orderBy: 'name', nameStartsWith: 'spi', url: '/characters' }))
65 | .resolves
66 | .toEqual(data);
67 | });
68 |
69 | it('Api.appendParameters - should append options to request url', () => {
70 | let result = api.appendParameters('/characters', { page: 1, orderBy: 'name', nameStartsWith: 'spi' });
71 |
72 | expect(result).toBe(`/characters?ts=1&apikey=######&hash=######&nameStartsWith=spi&orderBy=name&limit=15&offset=0`);
73 | });
74 |
75 | it('Api.getCharacters - should retrive error', async () => {
76 | const error = {
77 | stack: ''
78 | };
79 |
80 | axios.get.mockImplementationOnce(() => Promise.reject(error));
81 |
82 | api.version = 'v2';
83 |
84 | await expect(api.get({ page: 1, orderBy: 'name', nameStartsWith: 'spi', url: '/characters' }))
85 | .rejects
86 | .toEqual(error);
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to your Workbox-powered service worker!
3 | *
4 | * You'll need to register this file in your web app and you should
5 | * disable HTTP caching for this file too.
6 | * See https://goo.gl/nhQhGp
7 | *
8 | * The rest of the code is auto-generated. Please don't update this file
9 | * directly; instead, make changes to your Workbox build configuration
10 | * and re-run your build process.
11 | * See https://goo.gl/2aRDsh
12 | */
13 |
14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js");
15 |
16 | workbox.skipWaiting();
17 | workbox.clientsClaim();
18 |
19 | /**
20 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to
21 | * requests for URLs in the manifest.
22 | * See https://goo.gl/S9QRab
23 | */
24 | self.__precacheManifest = [
25 | {
26 | "url": "289.1d43c609c894bd6b91dc.js",
27 | "revision": "0ea11b46301c591a29d7c58576b41a09"
28 | },
29 | {
30 | "url": "577.1d43c609c894bd6b91dc.js",
31 | "revision": "4f84a27c44f73fcba43f9e69bda84098"
32 | },
33 | {
34 | "url": "76.1d43c609c894bd6b91dc.js",
35 | "revision": "3e614050b1177cbf94b12287d18ff703"
36 | },
37 | {
38 | "url": "788.1d43c609c894bd6b91dc.js",
39 | "revision": "6c23cce13cee9316e8fe4970c24d76ac"
40 | },
41 | {
42 | "url": "app.1d43c609c894bd6b91dc.js",
43 | "revision": "656097b08d79622bddafa968646339f5"
44 | },
45 | {
46 | "url": "fonts/font.woff",
47 | "revision": "45b47f3e9c7d74b80f5c6e0a3c513b23"
48 | },
49 | {
50 | "url": "images/close.svg",
51 | "revision": "e7fb89851b79caec1094b297281ca19b"
52 | },
53 | {
54 | "url": "images/dots.svg",
55 | "revision": "9524eabbca5125c6c23de2cebe1853d0"
56 | },
57 | {
58 | "url": "images/github.svg",
59 | "revision": "bd666487a62742a3d09429cbcb14a0cd"
60 | },
61 | {
62 | "url": "images/icons/safari-pinned-tab.svg",
63 | "revision": "9485bad24d046e48791ee8fe251ca808"
64 | },
65 | {
66 | "url": "images/marvel.svg",
67 | "revision": "3cb51f0cad379a7e38170621aa653d75"
68 | },
69 | {
70 | "url": "images/search.svg",
71 | "revision": "de5d7f019aae630499fa25b5a9b37d20"
72 | },
73 | {
74 | "url": "index.html",
75 | "revision": "bd05c8fe46ccfe09c8c29b118968bbb9"
76 | }
77 | ].concat(self.__precacheManifest || []);
78 | workbox.precaching.suppressWarnings();
79 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
80 |
81 | workbox.routing.registerRoute(/https\:\/\/gateway\.marvel\.com\/v1\/public\/.+/g, workbox.strategies.cacheFirst({ "cacheName":"api-cache", plugins: [new workbox.expiration.Plugin({"maxEntries":20,"maxAgeSeconds":36000,"purgeOnQuotaError":false}), new workbox.cacheableResponse.Plugin({"statuses":[0,200]})] }), 'GET');
82 | workbox.routing.registerRoute(/https\:\/\/gateway\.marvel\.com\/v1\/public\/.+/g, workbox.strategies.staleWhileRevalidate({ plugins: [new workbox.cacheableResponse.Plugin({"statuses":[0,200]})] }), 'GET');
83 |
--------------------------------------------------------------------------------
/test/misc/tilt.test.js:
--------------------------------------------------------------------------------
1 | import Tilt from '../../src/scripts/misc/tilt';
2 |
3 | describe('Tilt Class', () => {
4 | describe('init', () => {
5 | it('inits Tilt instance', () => {
6 | const tilt = new Tilt();
7 | const $el = document.createElement('div');
8 | const getBoundingClientRectSpy = jest.spyOn($el, 'getBoundingClientRect');
9 | const addListenersSpy = jest.spyOn(tilt, '_addListeners');
10 |
11 | tilt.init($el);
12 |
13 | expect(tilt.el).toEqual($el);
14 | expect(tilt.over).toBeFalsy();
15 | expect(getBoundingClientRectSpy).toBeCalled();
16 | expect(addListenersSpy).toBeCalled();
17 | });
18 | });
19 |
20 | describe('_addListeners', () => {
21 | it('adds events to the element', () => {
22 | const tilt = new Tilt();
23 | const $el = document.createElement('div');
24 | tilt.init($el);
25 |
26 | const addEventListenerSpy = jest.spyOn($el, 'addEventListener')
27 |
28 | tilt._addListeners();
29 |
30 | expect(addEventListenerSpy).toHaveBeenCalledTimes(3);
31 | });
32 | });
33 |
34 | describe('_mouseOver', () => {
35 | it('calls position when over is truthy', () => {
36 | const tilt = new Tilt();
37 | const $el = document.createElement('div');
38 | const positionSpy = jest.spyOn(tilt, '_position');
39 | const mouseOverEvent = new Event('mouseover');
40 |
41 | tilt.init($el);
42 | tilt.over = true;
43 | tilt.el.dispatchEvent(mouseOverEvent);
44 |
45 | expect(positionSpy).toBeCalled();
46 | });
47 | });
48 |
49 | describe('_onMouseMove', () => {
50 | it('calls position and transition on mouse move', () => {
51 | const tilt = new Tilt();
52 | const $el = document.createElement('div');
53 | const positionSpy = jest.spyOn(tilt, '_position');
54 | const transitionSpy = jest.spyOn(tilt, '_transition');
55 | const mouseMoveEvent = new Event('mousemove');
56 |
57 | tilt.init($el);
58 | tilt.el.dispatchEvent(mouseMoveEvent);
59 |
60 | expect(positionSpy).toBeCalled();
61 | expect(transitionSpy).toBeCalled();
62 | });
63 | });
64 |
65 | describe('_onMouseOut', () => {
66 | it('applies styles and sets over to false', () => {
67 | const tilt = new Tilt();
68 | const $el = document.createElement('div');
69 | const applyStyleSpy = jest.spyOn(tilt, '_applyStyle');
70 | const mouseOutEvent = new Event('mouseout');
71 |
72 | tilt.init($el);
73 | tilt.over = true;
74 | tilt.el.dispatchEvent(mouseOutEvent);
75 |
76 | expect(applyStyleSpy).toBeCalled();
77 | expect(tilt.over).toBeFalsy();
78 | });
79 | });
80 |
81 | describe('_convertToString', () => {
82 | it('converts an object with some style properties to string', () => {
83 | const tilt = new Tilt();
84 |
85 | const styleObj = {
86 | transform: 'rotate(90deg)',
87 | }
88 |
89 | const expectString = 'transform:rotate(90deg);';
90 |
91 | expect(tilt._convertToString(styleObj)).toEqual(expectString);
92 | });
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/src/scss/media-queries/desktop.scss:
--------------------------------------------------------------------------------
1 | @import '../base/easing';
2 |
3 | @media screen and (min-width: 960px ) {
4 | .grid {
5 | justify-content: center;
6 | }
7 |
8 | .about {
9 | .content {
10 | max-width: 520px;
11 | }
12 | }
13 |
14 | .search, .header {
15 | position: relative;
16 | }
17 |
18 | .header {
19 | box-shadow: -3px -5px 1px 5px rgba(0, 0, 0, .75);
20 |
21 | &__search {
22 | visibility: hidden;
23 | }
24 | }
25 |
26 | .menu {
27 | &.show {
28 | box-shadow: none;
29 | }
30 | }
31 |
32 | .search {
33 | box-shadow: -2px 2px 7px 0 rgba(0, 0, 0, .32);
34 | }
35 |
36 |
37 | .list {
38 | .show {
39 | box-shadow: -3px 9px 8px 0 rgba(0, 0, 0, .3);
40 | }
41 | }
42 |
43 | .slides {
44 | width: 100%;
45 |
46 | .first, .second {
47 | width: 100%;
48 | }
49 | }
50 |
51 | .detail {
52 | overflow-y: hidden;
53 |
54 | img {
55 | border-radius: 4px;
56 | max-width: 50vh;
57 | transform: translate(-50%, -50%) translate3d(0, 0, 0);
58 | width: initial;
59 | }
60 |
61 | &__cover {
62 | position: relative;
63 | width: 50%;
64 |
65 | &--reflex {
66 | filter: grayscale(0) blur(16px);
67 | opacity: .4;
68 | transform: scale(1.1);
69 | width: 100%;
70 | }
71 | }
72 |
73 | &__content {
74 | display: flex;
75 | flex-direction: column;
76 | flex-wrap: wrap;
77 | height: 100%;
78 | position: relative;
79 | }
80 |
81 | &__infos {
82 | background-color: #edeeef;
83 | box-shadow: -2px -3px 20px 0 rgba(0, 0, 0, .3);
84 | min-height: 100vh;
85 | overflow-y: scroll;
86 | position: relative;
87 | transform: translateY(0) translate3d(0, 0, 0);
88 | width: 50%;
89 | }
90 |
91 | .info {
92 | &__name {
93 | box-shadow: none;
94 | line-height: 1.9;
95 | padding: 50px;
96 |
97 | h2 {
98 | font-size: 30pt;
99 | text-transform: uppercase;
100 | }
101 | }
102 | }
103 | }
104 |
105 | .slides {
106 | overflow: hidden;
107 |
108 | .loader {
109 | width: 10%;
110 | }
111 | }
112 |
113 | .thumb {
114 | margin-right: 20px;
115 | width: 180px;
116 |
117 | &__title {
118 | font-size: 16px;
119 | }
120 |
121 | &__file {
122 | transform: scale(1);
123 | transition: filter .3s, transform .6s easing(easeOutQuart);
124 | }
125 |
126 | &:nth-child(3n + 0) {
127 | margin-right: 20px;
128 | }
129 |
130 | &:hover {
131 | .thumb__file {
132 | filter: brightness(15%);
133 | transform: scale(1.2);
134 | }
135 |
136 | .thumb__title {
137 | opacity: 1;
138 | transition: opacity .3s .2s;
139 | }
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/scripts/pages/detailComic.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Transition from '../components/transition';
4 | import BackButton from '../components/back-button';
5 | import ScrollIndicator from '../components/scroll-indicator';
6 | import Tilt from '../misc/tilt';
7 | import Cover from '../components/cover';
8 | import { animateIn, enableScroll, coverOnLoad, infoData } from './detailsCommon';
9 |
10 | class DetailComic extends PureComponent {
11 | constructor(props) {
12 | super(props);
13 | }
14 |
15 | positionInfos() {
16 | let viewH = 0;
17 | if (window.innerWidth < 960) {
18 | viewH = window.innerHeight - 50;
19 | }
20 | this.infos.style = `transform:translateY(${viewH}px)`;
21 | }
22 |
23 | componentDidMount() {
24 | this.tilt = new Tilt();
25 | this.mobile = true;
26 |
27 | let slides = [...document.querySelectorAll('.slides .first')];
28 |
29 | window.onresize = () => { this.positionInfos(); };
30 |
31 | const content = document.querySelector('.detail__content');
32 | const img = content.querySelector('img');
33 |
34 | img.onload = () => { coverOnLoad(content, img, slides, this.positionInfos.bind(this), this.tilt); };
35 |
36 | animateIn(slides);
37 | }
38 |
39 | componentWillUnmount() {
40 | window.onresize = null;
41 |
42 | enableScroll();
43 | }
44 |
45 | createMarkup(markup) {
46 | return { __html: markup }
47 | }
48 |
49 | onBackButtonClick() {
50 | this.props.history.goBack();
51 | }
52 |
53 | getDescription(description) {
54 | return description ?
: '';
55 | }
56 |
57 | hasItens(data) {
58 | let output = false;
59 | if (data.items && data.items.length) {
60 | output = true;
61 | }
62 |
63 | return output;
64 | }
65 |
66 | render() {
67 | return (
68 |
69 |
70 |
this.content = c} className="detail__content" >
71 |
72 |
73 |
this.infos = c} className="detail__infos">
74 |
75 |
76 |
this.title = c}>{this.props.selectedItem.title}
77 | {this.getDescription(this.props.selectedItem.description)}
78 |
79 | {infoData(this.props.selectedItem.creators, this.hasItens, 'Creators')}
80 | {infoData(this.props.selectedItem.characters, this.hasItens, 'Characters')}
81 | {infoData(this.props.selectedItem.stories, this.hasItens, 'Stories')}
82 | {infoData(this.props.selectedItem.series, this.hasItens, 'Series')}
83 |
84 |
85 |
86 |
87 | );
88 | }
89 | }
90 |
91 | DetailComic.propTypes = {
92 | selectedItem: PropTypes.object,
93 | history: PropTypes.object,
94 | }
95 |
96 | export default DetailComic;
97 |
--------------------------------------------------------------------------------
/src/scss/pages/detail.scss:
--------------------------------------------------------------------------------
1 | @import '../base/color';
2 | @import '../base/easing';
3 |
4 | .detail {
5 | background-color: #000000;
6 | color: color(invert);
7 | height: 100%;
8 | left: 0;
9 | overflow: scroll;
10 | overflow-x: hidden;
11 | position: fixed;
12 | top: 0;
13 | width: 100%;
14 | z-index: 12;
15 |
16 | &__content {
17 | opacity: 0;
18 | position: absolute;
19 | will-change: opacity;
20 |
21 | &.active {
22 | opacity: 1;
23 | }
24 | }
25 |
26 | img {
27 | background-color: color(darkest);
28 | border-radius: 2%;
29 | box-shadow: 0 2px 20px 0 rgba(0, 0, 0, .3);
30 | display: none;
31 | left: 50%;
32 | margin: 0 auto;
33 | position: absolute;
34 | top: 50%;
35 | transform: translate(-50%, -55%) translate3d(0, 0, 0);
36 | transform-origin: left top;
37 | width: 80%;
38 | z-index: 1;
39 |
40 | &.show {
41 | display: block;
42 | }
43 | }
44 |
45 | &__cover {
46 | display: block;
47 | height: 100%;
48 | perspective: 800px;
49 | position: fixed;
50 | width: 100vw;
51 | z-index: 1;
52 |
53 | &--reflex {
54 | background-origin: content-box;
55 | background-position: top center;
56 | background-repeat: no-repeat;
57 | background-size: cover;
58 | filter: grayscale(100%);
59 | height: 100%;
60 | margin: 0 auto;
61 | opacity: .1;
62 | overflow: hidden;
63 | position: absolute;
64 | top: 0;
65 | width: inherit;
66 |
67 | &::before {
68 | background-image: linear-gradient(160deg, rgba(255, 255, 255, .15) 50%, transparent 50%);
69 | content: '';
70 | height: 100%;
71 | position: absolute;
72 | top: 0;
73 | width: 100%;
74 | z-index: 1;
75 | }
76 | }
77 | }
78 |
79 | &__infos {
80 | background-color: color(info);
81 | font-size: 16px;
82 | line-height: 1.3;
83 | position: absolute;
84 | transform: translateY(100vh) translate3d(0, 0, 0);
85 | width: 100vw;
86 | z-index: 1;
87 |
88 | section{
89 | background-color: color(series);
90 | color: color(secondary);
91 |
92 | li {
93 | &:nth-child(even) {
94 | background-color: rgba(12, 12, 12, 0.05);
95 | }
96 |
97 | &:nth-child(odd) {
98 | background-color: rgba(0, 0, 0, 0.02);
99 | }
100 | }
101 | }
102 |
103 | }
104 |
105 | .info {
106 | &__name {
107 | background-color: color(secondary);
108 | box-shadow: -2px 18px 20px 20px color(shadow);
109 | padding: 15px;
110 | }
111 |
112 | &__series, &__stories, &__creators, &__characters {
113 | ul {
114 | list-style: inside decimal;
115 | }
116 |
117 | .sub-title {
118 | padding: 15px 10px;
119 | }
120 |
121 | li {
122 | padding: 10px 20px;
123 | }
124 | }
125 | }
126 | }
127 |
128 | .disable-scroll {
129 | overflow: hidden;
130 | }
131 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marvel-api-explorer",
3 | "version": "0.0.1",
4 | "main": "",
5 | "author": "Ion D. FIlho ",
6 | "engines": {
7 | "node": "15.11.0",
8 | "npm": "7.6.0"
9 | },
10 | "license": "MIT",
11 | "scripts": {
12 | "preinstall": "npx npm-force-resolutions",
13 | "prod": "cross-env NODE_ENV=production ./node_modules/.bin/webpack --config webpack.prod.js",
14 | "start": "cross-env NODE_ENV=development ./node_modules/.bin/webpack-dev-server --open --config webpack.dev.js",
15 | "test": "jest --coverage --testMatch '**/*.test.js'",
16 | "test:watch": "npm run test -- --watch",
17 | "workbox": "./node_modules/.bin/workbox generateSW workbox-config.js"
18 | },
19 | "jest": {
20 | "notify": false,
21 | "clearMocks": true,
22 | "restoreMocks": true,
23 | "collectCoverage": true,
24 | "collectCoverageFrom": [
25 | "src/**/*.{js,jsx,ts}"
26 | ]
27 | },
28 | "resolutions": {
29 | "yargs-parser": ">=18.1.2",
30 | "braces": ">=3.0.2",
31 | "dot-prop": ">=5.1.1"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git@github.com:iondrimba/marvel-api-explorer.git"
36 | },
37 | "dependencies": {
38 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
39 | "axios": "^0.21.1",
40 | "babel-preset-env": "^1.7.0",
41 | "connected-react-router": "^6.9.1",
42 | "history": "^4.10.1",
43 | "npm-force-resolutions": "0.0.10",
44 | "prop-types": "^15.7.2",
45 | "react-router": "^5.2.0",
46 | "react-router-redux": "^4.0.8",
47 | "redux-thunk": "^2.1.0",
48 | "reselect": "^4.0.0",
49 | "webpack-merge": "^5.7.3",
50 | "workbox-cli": "^3.6.3"
51 | },
52 | "devDependencies": {
53 | "@babel/cli": "^7.13.10",
54 | "@babel/core": "^7.13.10",
55 | "@babel/plugin-proposal-object-rest-spread": "^7.13.8",
56 | "@babel/plugin-transform-runtime": "^7.13.10",
57 | "@babel/preset-env": "^7.14.1",
58 | "@babel/preset-react": "^7.12.13",
59 | "@babel/register": "^7.13.16",
60 | "autoprefixer": "10.2.5",
61 | "babel-core": "^7.0.0-bridge.0",
62 | "babel-eslint": "^10.1.0",
63 | "babel-jest": "^26.6.3",
64 | "babel-loader": "^8.2.2",
65 | "babelify": "^10.0.0",
66 | "browserify": "^17.0.0",
67 | "browserify-istanbul": "^3.0.1",
68 | "clean-webpack-plugin": "^3.0.0",
69 | "compression-webpack-plugin": "^6.1.1",
70 | "copy-webpack-plugin": "^8.0.0",
71 | "cross-env": "^7.0.3",
72 | "css-loader": "^5.1.3",
73 | "enzyme": "^3.11.0",
74 | "eslint": "^7.21.0",
75 | "eslint-loader": "^4.0.2",
76 | "eslint-plugin-jest": "^24.3.2",
77 | "eslint-plugin-react": "^7.22.0",
78 | "file-loader": "^6.2.0",
79 | "hammerjs": "^2.0.8",
80 | "html-loader": "^2.1.2",
81 | "html-webpack-plugin": "^4.5.2",
82 | "image-webpack-loader": "^7.0.1",
83 | "jest": "^26.6.3",
84 | "json-loader": "^0.5.4",
85 | "mini-css-extract-plugin": "^1.3.9",
86 | "node-sass": "^5.0.0",
87 | "postcss-cssnext": "^3.1.0",
88 | "postcss-loader": "^4.2.0",
89 | "promise-polyfill": "^8.2.0",
90 | "raw-loader": "^4.0.2",
91 | "react": "^17.0.1",
92 | "react-dom": "^17.0.1",
93 | "react-redux": "^7.2.2",
94 | "react-router-dom": "^5.2.0",
95 | "redux": "^4.0.5",
96 | "reselect": "^4.0.0",
97 | "resolve-url-loader": "^3.1.3",
98 | "sass-loader": "^11.0.1",
99 | "style-loader": "^2.0.0",
100 | "stylefmt": "^6.0.3",
101 | "stylelint-config-standard": "^21.0.0",
102 | "stylelint-webpack-plugin": "^2.1.1",
103 | "sw-precache-webpack-plugin": "^1.0.0",
104 | "system": "^2.0.1",
105 | "url-loader": "^4.1.1",
106 | "webpack": "^5.36.1",
107 | "webpack-cli": "^4.7.0",
108 | "webpack-dev-server": "^3.11.2",
109 | "workbox-sw": "^6.1.2",
110 | "workbox-webpack-plugin": "^6.1.2"
111 | }
112 | }
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 |
6 | const config = {
7 | resolve: {
8 | extensions: ['.js', '.jsx', '.json', '.mp3', '.ico']
9 | },
10 | entry: {
11 | app: './src/scripts/app'
12 | },
13 | output: {
14 | path: __dirname + '/public',
15 | filename: '[name].[hash].js'
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /.j(s|sx)$/,
21 | exclude: /node_modules/,
22 | use: ['babel-loader', 'eslint-loader'],
23 | },
24 | {
25 | test: /\.html$/,
26 | use: ['raw-loader']
27 | },
28 | {
29 | test: /\.css$/,
30 | use: [
31 | { loader: 'style-loader' },
32 | {
33 | loader: 'css-loader',
34 | options: {
35 | modules: {
36 | mode: 'local',
37 | localIdentName: '[local]',
38 | },
39 | importLoaders: true,
40 | sourceMap: true,
41 | localIdentName: '[local]',
42 | }
43 | }
44 | ]
45 | },
46 | {
47 | test: /\.scss$/,
48 | use: [
49 | { loader: 'style-loader' },
50 | {
51 | loader: 'css-loader',
52 | options: {
53 | modules: {
54 | mode: 'local',
55 | localIdentName: '[local]',
56 | },
57 | import: true,
58 | importLoaders: true,
59 | }
60 | },
61 | { loader: 'sass-loader' }
62 | ]
63 | },
64 | {
65 | test: /\.(eot|ttf|woff|woff2)$/,
66 | loader: 'file-loader',
67 | options: {
68 | name: 'fonts/[name].[ext]',
69 | }
70 | },
71 | {
72 | test: /\.(mp3)$/,
73 | loader: 'file-loader',
74 | options: {
75 | name: 'sounds/[name].[ext]',
76 | }
77 | },
78 | {
79 | test: /.*\.(gif|png|jpe?g|svg)$/i,
80 | use: [
81 | {
82 | loader: 'file-loader',
83 | options: {
84 | hash: 'sha512',
85 | digest: 'hex',
86 | name: 'images/[name].[hash].[ext]',
87 | }
88 | },
89 | {
90 | loader: 'image-webpack-loader',
91 | options: {
92 | query: {
93 | mozjpeg: {
94 | progressive: true,
95 | },
96 | gifsicle: {
97 | interlaced: true,
98 | },
99 | optipng: {
100 | optimizationLevel: 7,
101 | }
102 | }
103 | }
104 | }
105 | ]
106 | }
107 | ]
108 | },
109 |
110 | plugins: [
111 | new CleanWebpackPlugin(),
112 | new HtmlWebpackPlugin({
113 | title: 'Marvel API Demo',
114 | template: './src/index.html',
115 | inject: 'body'
116 | }),
117 | new CopyWebpackPlugin({
118 | patterns: [
119 | {
120 | from: 'src/manifest.webmanifest', to: 'manifest.webmanifest'
121 | },
122 | {
123 | from: 'src/.htaccess'
124 | },
125 | {
126 | from: 'src/favicon.ico', to: 'favicon.ico'
127 | },
128 | {
129 | from: './robots.txt', to: 'robots.txt'
130 | },
131 | {
132 | from: 'src/browserconfig.xml', to: 'browserconfig.xml'
133 | },
134 | {
135 | from: 'src/favicon-16x16.png', to: 'favicon-16x16.png'
136 | },
137 | {
138 | from: 'src/favicon-32x32.png', to: 'favicon-32x32.png'
139 | },
140 | {
141 | from: 'src/favicon-48x48.png', to: 'favicon-48x48.png'
142 | },
143 | {
144 | from: 'src/images', to: 'images'
145 | },
146 | ],
147 | }),
148 | new webpack.EnvironmentPlugin([
149 | 'NODE_ENV'
150 | ]),
151 | new webpack.DefinePlugin({
152 | 'process.env': {
153 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV),
154 | 'PUBLIC_API_KEY': JSON.stringify(process.env.PUBLIC_API_KEY),
155 | }
156 | }),
157 | ]
158 | };
159 |
160 | module.exports = config;
161 |
--------------------------------------------------------------------------------
/public/577.1d43c609c894bd6b91dc.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkmarvel_api_explorer=self.webpackChunkmarvel_api_explorer||[]).push([[577],{6577:(t,e,n)=>{n.r(e),n.d(e,{default:()=>j});var r=n(1225),o=n(6347),i=n(6540),c=n(5556),u=n.n(c),a=n(9399),l=n(5022),s=n(8557),f=n(2133),p=n(8277),y=n(884);function m(t){return m="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},m(t)}function d(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n
--------------------------------------------------------------------------------
/src/images/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/788.1d43c609c894bd6b91dc.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkmarvel_api_explorer=self.webpackChunkmarvel_api_explorer||[]).push([[788],{9788:(t,e,n)=>{n.r(e),n.d(e,{default:()=>S});var r=n(1225),o=n(6347),i=n(6540),c=n(5556),u=n.n(c),a=n(9399),s=n(5022),l=n(2133),f=n(8277),p=n(8557),m=n(884);function y(t){return y="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},y(t)}function d(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n{r.r(t),r.d(t,{default:()=>O});var n=r(6540),o=r(5556),i=r.n(o);function c(e){return c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},c(e)}function u(e,t){for(var r=0;rMarvel API Demo This Application is ready to go offline!
almost there, hang on...
--------------------------------------------------------------------------------
/src/scripts/container/homeContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Home from '../pages/home';
3 | import { fetch } from '../actions/fetch';
4 | import fetching from '../actions/fetching';
5 | import search from '../actions/search';
6 | import fetchingError from '../actions/fetchingError';
7 | import filter from '../actions/filter';
8 | import started from '../actions/started';
9 | import menuOpen from '../actions/menuOpen';
10 | import pagination from '../actions/pagination';
11 | import appStore from '../model/store';
12 | import defaultStore from '../model/initialState';
13 | import PaginationHelper from '../model/paginationHelper';
14 |
15 | const pg = new PaginationHelper();
16 |
17 | function mapStateToProps(store) {
18 | return {
19 | menuOpen: store.menuOpen,
20 | error: store.error,
21 | fetching: store.fetching,
22 | started: store.started,
23 | filter: store.filter,
24 | search: store.search,
25 | pagination: Object.assign({},
26 | store.pagination, {
27 | pages: pg.getPages(store.pagination),
28 | next: pg.hasNext(store.pagination),
29 | prev: pg.hasPrev(store.pagination)
30 | }
31 | ),
32 | data: store.data
33 | };
34 | }
35 |
36 | function getQueryString(search) {
37 | return search ? `?search=${search}` : '';
38 | }
39 |
40 | function paginate(url, dispatch, props) {
41 | const page = Number(url.split('/')[2].split('?')[0]);
42 | const store = appStore.getState();
43 |
44 | dispatch(pagination({
45 | current: page,
46 | pages: pg.getPages(store.pagination),
47 | next: pg.hasNext(store.pagination),
48 | prev: pg.hasPrev(store.pagination)
49 | }));
50 |
51 | props.history.push(url);
52 | }
53 |
54 | function _errorClear(props, dispatch) {
55 | dispatch(fetchingError({ code: '' }));
56 | }
57 |
58 | function _firstFetch(props, dispatch, fetchData) {
59 | let searchTerm = props.search;
60 | let page = props.pagination.current;
61 | let type = props.filter;
62 |
63 | const paths = props.location.pathname.split('/');
64 |
65 | if (paths.length && props.location.pathname !== '/') {
66 | type = paths[1];
67 |
68 | if (props.location.search) {
69 | searchTerm = props.location.search.replace(/\?search=/g, '');
70 | }
71 | }
72 |
73 | props.history.push(`/${type}/${page}${getQueryString(searchTerm)}`);
74 |
75 | dispatch(search(searchTerm));
76 | dispatch(started(true));
77 |
78 | if (!isNaN(paths[2])) {
79 | page = Number(paths[2]);
80 |
81 | dispatch(filter(type));
82 | dispatch(pagination(Object.assign({}, defaultStore.pagination, { current: page })));
83 | }
84 |
85 | fetchData(type, Object.assign({}, defaultStore.pagination, { current: page }), searchTerm, dispatch);
86 | }
87 |
88 | function _paginataionAction(delta, props, dispatch) {
89 | const url = `/${props.filter}/${Number(props.pagination.current) + delta}${getQueryString(props.search)}`;
90 |
91 | paginate(url, dispatch, props);
92 | }
93 |
94 | function _fetchData(filter, pagination, search, dispatch) {
95 | let aditionalOptions = {
96 | orderBy: 'name',
97 | nameStartsWith: search
98 | };
99 |
100 | if (filter === 'comics') {
101 | aditionalOptions = {
102 | orderBy: 'title',
103 | titleStartsWith: search
104 | };
105 | }
106 | dispatch(fetch(Object.assign({}, { url: `/${filter}`, page: pagination.current, total: pagination.total }, aditionalOptions)));
107 | dispatch(fetchingError({ code: '' }));
108 | dispatch(fetching(true));
109 | }
110 |
111 | const mapDispatchToProps = (dispatch, store) => {
112 | return {
113 | errorClear: props => _errorClear(props, dispatch),
114 |
115 | firstFetch(props) {
116 | _firstFetch(props, dispatch, _fetchData);
117 | },
118 |
119 | fetchAction(page = 0) {
120 | if (page) {
121 | Object.assign(appStore.getState().pagination, { current: page });
122 | }
123 |
124 | _fetchData(appStore.getState().filter, appStore.getState().pagination, appStore.getState().search, dispatch);
125 | },
126 |
127 | searchAction: (val, props) => {
128 | props.history.push(`/${props.filter}/${defaultStore.pagination.current}${getQueryString(val)}`);
129 |
130 | dispatch(search(val));
131 | dispatch(menuOpen(false));
132 | dispatch(started(true));
133 | dispatch(pagination(defaultStore.pagination));
134 |
135 | _fetchData(appStore.getState().filter, appStore.getState().pagination, val, dispatch);
136 | },
137 |
138 | searchClear: (val) => {
139 | dispatch(search(val));
140 | },
141 |
142 | filterAction: (val, props) => {
143 | props.history.push(`/${val}/${defaultStore.pagination.current}?search=${appStore.getState().search}`);
144 |
145 | dispatch(filter(val));
146 | dispatch(pagination(defaultStore.pagination));
147 |
148 | _fetchData(val, appStore.getState().pagination, appStore.getState().search, dispatch);
149 | },
150 |
151 | paginationAction: (url, props) => {
152 | paginate(url, dispatch, props);
153 | },
154 |
155 | paginationPrevAction: (props) => {
156 | pg.hasPrev(props.pagination) ? _paginataionAction(-1, props, dispatch) : null;
157 | },
158 |
159 | paginationNextAction: (props) => {
160 | pg.hasNext(props.pagination) ? _paginataionAction(+1, props, dispatch) : null;
161 | },
162 |
163 | toogleMenuAction: (visible) => {
164 | dispatch(menuOpen(visible));
165 | }
166 | };
167 | }
168 |
169 | export default connect(mapStateToProps, mapDispatchToProps)(Home);
170 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Marvel API Demo
61 |
84 |
85 |
117 |
118 |
119 |
130 |
131 |
132 |
133 | This Application is ready to go offline!
134 |
135 | almost there, hang on...
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/test/model/paginationHelper.test.js:
--------------------------------------------------------------------------------
1 | import PaginationHelper from '../../src/scripts/model/paginationHelper';
2 |
3 | describe('paginationHelper', () => {
4 | describe('mountGroups', () => {
5 | const helper = new PaginationHelper();
6 |
7 | it('always return an array when mountGroups is called when any integer number is passed', () => {
8 | expect(Array.isArray(helper.mountGroups(1))).toBe(true);
9 | expect(Array.isArray(helper.mountGroups(5))).toBe(true);
10 | expect(Array.isArray(helper.mountGroups(10))).toBe(true);
11 | expect(Array.isArray(helper.mountGroups(53))).toBe(true);
12 | });
13 |
14 | it('every element of the returned array should be an array', () => {
15 | helper.mountGroups(16).forEach(group => {
16 | expect(Array.isArray(group)).toBe(true);
17 | })
18 | });
19 |
20 | describe('every element(group) of the returned array should..', () => {
21 | it('be a group with a max size of 5 elements', () => {
22 | const lengthHigherThan5 = group => (group || []).length > 5;
23 | const anyGroupWithMoreThan5Elements =
24 | helper.mountGroups(13).every(lengthHigherThan5);
25 |
26 | expect(anyGroupWithMoreThan5Elements).toBe(false)
27 | });
28 |
29 | it('every element of the group should be a number', () => {
30 | const isNumber = element => typeof element === 'number';
31 |
32 | helper.mountGroups(30).forEach(group => {
33 | expect(group.every(isNumber)).toBe(true);
34 | });
35 | });
36 |
37 | it('elements is sequencial', () => {
38 | const isSequencial = (cur, i, group) => {
39 | if (group[i + 1]) {
40 | cur < group[i + 1];
41 | }
42 |
43 | return true;
44 | };
45 |
46 | expect(helper.mountGroups(4).every(isSequencial)).toBe(true);
47 | });
48 | });
49 | });
50 |
51 | describe('getCurrentGroup', () => {
52 | const helper = new PaginationHelper();
53 | const groups = helper.mountGroups(11);
54 |
55 | it('returns [0, 1, 2, 3, 4] when current position is 0', () => {
56 | expect(helper.getCurrentGroup(groups, 0)).toEqual([0, 1, 2, 3, 4]);
57 | });
58 |
59 | it('returns [5, 6, 7, 8, 9] when current position is 1', () => {
60 | expect(helper.getCurrentGroup(groups, 1)).toEqual([5, 6, 7, 8, 9]);
61 | });
62 |
63 | it('returns [10] when current position is 2', () => {
64 | expect(helper.getCurrentGroup(groups, 2)).toEqual([10]);
65 | });
66 | });
67 |
68 | describe('groupPages', () => {
69 | const helper = new PaginationHelper();
70 |
71 | it('returns 0 when currentPage < 5', () => {
72 | expect(helper.groupPages(1)).toEqual(0);
73 | expect(helper.groupPages(3)).toEqual(0);
74 | expect(helper.groupPages(4)).toEqual(0);
75 | });
76 |
77 | it('returns 1 when currentPage > 5 and < 10', () => {
78 | expect(helper.groupPages(6)).toEqual(1);
79 | expect(helper.groupPages(8)).toEqual(1);
80 | expect(helper.groupPages(9)).toEqual(1);
81 | });
82 | });
83 |
84 | describe('getPages', () => {
85 | const helper = new PaginationHelper();
86 |
87 | it('returns an empty array when totalElement <= 1', () => {
88 | expect(helper.getPages({ total: 0, current: 1 })).toEqual([]);
89 | expect(helper.getPages({ total: 1, current: 1 })).toEqual([]);
90 | });
91 |
92 | it('returns [0, 1, 2, 3, 4] when currentPagination <= 5', () => {
93 | expect(helper.getPages({ total: 5, current: 1 })).toEqual([0, 1, 2, 3, 4]);
94 | expect(helper.getPages({ total: 5, current: 5 })).toEqual([0, 1, 2, 3, 4]);
95 | });
96 |
97 | it('returns [5, 6, 7, 8, 9] when currentPagination < 5 and <= 10', () => {
98 | expect(helper.getPages({ total: 10, current: 6 })).toEqual([5, 6, 7, 8, 9]);
99 | expect(helper.getPages({ total: 10, current: 10 })).toEqual([5, 6, 7, 8, 9]);
100 | });
101 | });
102 |
103 | describe('getTotalPages', () => {
104 | const helper = new PaginationHelper();
105 |
106 | it('returns 2 when totalItens = 10 and maxPages = 5', () => {
107 | expect(helper.getTotalPages(10, 5)).toEqual(2);
108 | });
109 |
110 | it('returns 20 when totalItens = 100 and maxPages = 5', () => {
111 | expect(helper.getTotalPages(100, 5)).toEqual(20);
112 | });
113 |
114 | it('returns 200 when totalItens = 1000 and maxPages = 5', () => {
115 | expect(helper.getTotalPages(1000, 5)).toEqual(200);
116 | });
117 | });
118 |
119 | describe('hasNext', () => {
120 | const helper = new PaginationHelper();
121 |
122 | it('returns false when totalPagination <= 1', () => {
123 | expect(helper.hasNext({ total: 0, current: 1 })).toEqual(false);
124 | expect(helper.hasNext({ total: 1, current: 1 })).toEqual(false);
125 | });
126 |
127 | it('returns false when totalPagination <= current', () => {
128 | expect(helper.hasNext({ total: 3, current: 5 })).toEqual(false);
129 | expect(helper.hasNext({ total: 4, current: 4 })).toEqual(false);
130 | });
131 |
132 | it('returns true when totalPagination > current', () => {
133 | expect(helper.hasNext({ total: 10, current: 5 })).toEqual(true);
134 | expect(helper.hasNext({ total: 4, current: 3 })).toEqual(true);
135 | });
136 | });
137 |
138 | describe('hasPrev', () => {
139 | const helper = new PaginationHelper();
140 |
141 | it('returns true when totalPagination > 0 and current > 1', () => {
142 | expect(helper.hasPrev({ total: 1, current: 2 })).toEqual(true);
143 | expect(helper.hasPrev({ total: 10, current: 5 })).toEqual(true);
144 | expect(helper.hasPrev({ total: 4, current: 3 })).toEqual(true);
145 | });
146 |
147 | it('returns false when totalPagination < 0 and current <= 1', () => {
148 | expect(helper.hasPrev({ total: 0, current: 1 })).toEqual(false);
149 | expect(helper.hasPrev({ total: 0, current: 0 })).toEqual(false);
150 | });
151 | });
152 |
153 | describe('getPrev', () => {
154 | const helper = new PaginationHelper();
155 |
156 |
157 | it('returns previousPage(current - 1) when totalPagination > 0 and current > 1', () => {
158 | expect(helper.getPrev({ total: 10, current: 5 })).toEqual(4);
159 | expect(helper.getPrev({ total: 4, current: 3 })).toEqual(2);
160 | });
161 |
162 |
163 | it('returns false when totalPagination < 0 and current <= 11', () => {
164 | expect(helper.getPrev({ total: 0, current: 1 })).toEqual(undefined);
165 | expect(helper.getPrev({ total: 0, current: 0 })).toEqual(undefined);
166 | });
167 | });
168 |
169 | describe('getNext', () => {
170 | const helper = new PaginationHelper();
171 |
172 |
173 | it('returns nextPage(current + 1) when totalPagination > 0 and current > 1', () => {
174 | expect(helper.getNext({ total: 10, current: 5 })).toEqual(6);
175 | expect(helper.getNext({ total: 4, current: 3 })).toEqual(4);
176 | });
177 |
178 |
179 | it('returns undefined when totalPagination < 0 and current <= 11', () => {
180 | expect(helper.getNext({ total: 0, current: 1 })).toEqual(undefined);
181 | expect(helper.getNext({ total: 0, current: 0 })).toEqual(undefined);
182 | });
183 | });
184 | });
185 |
--------------------------------------------------------------------------------
/public/289.1d43c609c894bd6b91dc.js:
--------------------------------------------------------------------------------
1 | "use strict";(self.webpackChunkmarvel_api_explorer=self.webpackChunkmarvel_api_explorer||[]).push([[289],{884:(t,e,n)=>{n.d(e,{HY:()=>v,l2:()=>m,Vn:()=>b,Dl:()=>h});var r=n(6540),o=n(5556),i=n.n(o);function u(t){return u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},u(t)}function c(t,e){for(var n=0;n{n.d(e,{A:()=>f});var r=n(6540);function o(t){return o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o(t)}function i(t,e){for(var n=0;n{n.d(e,{Mz:()=>c});var r="NOT_FOUND",o=function(t,e){return t===e};function i(t,e){var n,i,u="object"==typeof e?e:{equalityCheck:e},c=u.equalityCheck,a=void 0===c?o:c,l=u.maxSize,f=void 0===l?1:l,s=u.resultEqualityCheck,p=function(t){return function(e,n){if(null===e||null===n||e.length!==n.length)return!1;for(var r=e.length,o=0;o-1){var i=n[o];return o>0&&(n.splice(o,1),n.unshift(i)),i.value}return r}return{get:o,put:function(e,i){o(e)===r&&(n.unshift({key:e,value:i}),n.length>t&&n.pop())},getEntries:function(){return n},clear:function(){n=[]}}}(f,p);function v(){var e=y.get(arguments);if(e===r){if(e=t.apply(null,arguments),s){var n=y.getEntries().find(function(t){return s(t.value,e)});n&&(e=n.value)}y.put(arguments,e)}return e}return v.clearCache=function(){return y.clear()},v}function u(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r{n.d(e,{A:()=>y});var r=n(6540),o=n(5556),i=n.n(o);function u(t){return u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},u(t)}function c(t,e){for(var n=0;n{function r(t){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}function o(t,e){for(var n=0;nu});const u=function(){return t=function t(){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.offset=.1,this.animation=300},(e=[{key:"init",value:function(t){this.el=t,this.rect=this.el.getBoundingClientRect(),this.over=!1,this.timer,this._addListeners()}},{key:"_addListeners",value:function(){this.el.addEventListener("mouseover",this._onMouseOver.bind(this)),this.el.addEventListener("mousemove",this._onMouseMove.bind(this)),this.el.addEventListener("mouseout",this._onMouseOut.bind(this))}},{key:"_onMouseOver",value:function(t){this.over&&this._position(t,t.currentTarget)}},{key:"_onMouseMove",value:function(t){this._position(t,t.currentTarget),this._transition(t.currentTarget)}},{key:"_onMouseOut",value:function(t){this._applyStyle(t.currentTarget,{transform:"rotateX(0deg) rotateY(0deg) translate(-50%, -50%)"}),this.over=!1}},{key:"_convertToString",value:function(t){var e="";for(var n in t)e+="".concat(n,":").concat(t[n],";");return e}},{key:"_applyStyle",value:function(t,e){t.style=this._convertToString(e)}},{key:"_position",value:function(t,e){var n=this.rect,r=this.offset,o=-(t.offsetX-.5*n.width)*r,i=.03*(t.offsetY-.5*n.height);this._applyStyle(e,{transform:"rotateX("+i+"deg) rotateY("+o+"deg) translate(-50%, -50%)"})}},{key:"_transition",value:function(){var t=this;void 0!==this.timer&&clearTimeout(this.timer),this.timer=setTimeout(function(){t.over=!0},this.animation)}}])&&o(t.prototype,e),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e}()},8557:(t,e,n)=>{n.d(e,{A:()=>y});var r=n(6540),o=n(5556),i=n.n(o);function u(t){return u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},u(t)}function c(t,e){for(var n=0;n{n.d(e,{A:()=>o});var r=n(6540);const o=function(){return r.createElement("div",{className:"slides"},r.createElement("svg",{className:"loader",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 654.5 249.8"},r.createElement("path",{fill:"none",d:"M0 0h654.5v249.8H0z"}),r.createElement("path",{fill:"#fff",d:"M560.7 52.5V11H445l-19 138.8L407.4 11h-41.7l4.7 37c-4.8-9.5-22-37-59.4-37h-42v202.2L238.6 11H184l-31.4 209.5V11h-52.3L81.5 128.7 63.2 11H11v227.8h41V129L71 238.8h22L111 129v109.8h79.5l4.8-35h32l4.7 35h78v-74l9.6-1.4 19.8 75.4h40.3l-26-88.4c13.2-9.7 28-34.4 24-58L402.4 239l48-.2L483 32.3v206.5h77.7v-41h-37v-52.2h37V104h-37V52.5zM200 167.5l11.4-97.3 11.7 97.4h-23zm119.7-45a22.5 22.5 0 0 1-9.7 2.4V51.6h.2c3.3 0 27.3 1 27.3 36.2 0 18.3-8.2 30-17.8 34.6zm324 75.2v41h-76V11h41v186.8h35z"})),r.createElement("div",{className:"first"}),r.createElement("div",{className:"second"}))}}}]);
--------------------------------------------------------------------------------