├── public ├── _redirects ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── components │ ├── characterCard │ │ ├── CharacterCard.scss │ │ └── CharacterCard.tsx │ ├── backButton │ │ ├── BackButton.scss │ │ └── BackButton.tsx │ └── pagination │ │ └── Pagination.tsx ├── react-app-env.d.ts ├── App.scss ├── models │ ├── query.interface.ts │ ├── pagination.interface.ts │ ├── characters.interface.ts │ └── character.interface.ts ├── assets │ ├── img │ │ └── logo.png │ ├── variables │ │ └── variables.scss │ └── typography │ │ ├── karbon-regular-webfont.ttf │ │ ├── karbon-regular-webfont.woff │ │ ├── karbon-semibold-webfont.ttf │ │ └── karbon-semibold-webfont.woff ├── modules │ ├── Characters │ │ ├── Characters.scss │ │ ├── Characters.tsx │ │ └── provider │ │ │ └── characters.provider.tsx │ └── Character │ │ ├── Character.scss │ │ ├── provider │ │ └── character.provider.tsx │ │ └── Character.tsx ├── domain │ ├── PayloadAbstract.ts │ └── characters │ │ ├── CharactersQuery.ts │ │ └── Characters.mapper.ts ├── setupTests.ts ├── App.tsx ├── api │ └── AxiosClient.ts ├── App.test.tsx ├── index.scss ├── reportWebVitals.ts ├── index.tsx ├── services │ ├── Common.ts │ └── CharactersService.ts ├── Layout │ ├── Layout.scss │ └── Layout.tsx ├── hooks │ └── useFilters.ts └── logo.svg ├── .env ├── tsconfig.json ├── package.json └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/components/characterCard/CharacterCard.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_BASE_URL=https://rickandmortyapi.com/api 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | .App { 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/public/logo512.png -------------------------------------------------------------------------------- /src/models/query.interface.ts: -------------------------------------------------------------------------------- 1 | export interface QueryType { 2 | page: number, 3 | search: String | null 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/modules/Characters/Characters.scss: -------------------------------------------------------------------------------- 1 | .ant-list-item-extra { 2 | img { 3 | width: 100px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/PayloadAbstract.ts: -------------------------------------------------------------------------------- 1 | export class PayloadAbstract { 2 | page: Number | undefined; 3 | search: String | undefined; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/variables/variables.scss: -------------------------------------------------------------------------------- 1 | //------------- COLOR VARIABLES ------------- 2 | $white: #FFFFFF; 3 | $primary: #202329; 4 | $content-min-height: 280px; 5 | 6 | -------------------------------------------------------------------------------- /src/assets/typography/karbon-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/src/assets/typography/karbon-regular-webfont.ttf -------------------------------------------------------------------------------- /src/assets/typography/karbon-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/src/assets/typography/karbon-regular-webfont.woff -------------------------------------------------------------------------------- /src/assets/typography/karbon-semibold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/src/assets/typography/karbon-semibold-webfont.ttf -------------------------------------------------------------------------------- /src/assets/typography/karbon-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somekindofwallflower/rick-and-morty-app/master/src/assets/typography/karbon-semibold-webfont.woff -------------------------------------------------------------------------------- /src/models/pagination.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationType { 2 | count: number 3 | next: string | null 4 | pages: number 5 | prev: string | null 6 | } 7 | -------------------------------------------------------------------------------- /src/components/backButton/BackButton.scss: -------------------------------------------------------------------------------- 1 | @import 'src/assets/variables/variables'; 2 | 3 | .backButton { 4 | a.ant-typography { 5 | color: $primary; 6 | font-weight: bold; 7 | font-size: 18px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/models/characters.interface.ts: -------------------------------------------------------------------------------- 1 | import {CharacterType} from "src/models/character.interface"; 2 | import {PaginationType} from "src/models/pagination.interface"; 3 | 4 | export interface CharactersType { 5 | info: PaginationType, 6 | results: CharacterType[] 7 | } 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DefaultLayout from 'src/Layout/Layout' 3 | import './App.scss'; 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /src/domain/characters/CharactersQuery.ts: -------------------------------------------------------------------------------- 1 | export class CharactersQuery { 2 | page: Number = 1; 3 | search: String | undefined; 4 | 5 | static get FILTER_KEYS() { 6 | return { 7 | PAGE: "page", 8 | SEARCH: "search" 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/api/AxiosClient.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | /** 4 | * @description Create a axios instance 5 | */ 6 | export const AxiosClient = axios.create({ 7 | baseURL: process.env.REACT_APP_API_BASE_URL, 8 | headers: { 9 | "Content-type": "application/json" 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/modules/Character/Character.scss: -------------------------------------------------------------------------------- 1 | .image-wrapper { 2 | text-align: center; 3 | img { 4 | border-radius: 16px; 5 | } 6 | } 7 | 8 | .ant-tag { 9 | padding: 5px 10px 5px 10px !important; 10 | margin-bottom: 5px !important; 11 | } 12 | .episode-wrapper { 13 | padding-top: 14px; 14 | } 15 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Karbon Regular; 3 | margin: 0; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | } 7 | 8 | #root { 9 | height: 100%; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.scss'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/pagination/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import { Pagination } from 'antd'; 2 | 3 | interface Props { 4 | currentPage?: number, 5 | total?: number, 6 | onChange: any, 7 | showSizeChanger?: boolean 8 | } 9 | 10 | 11 | export const BasicPagination = ({ currentPage, total = 0, onChange, showSizeChanger = false}: Props) => { 12 | return ( 13 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/backButton/BackButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from 'antd'; 3 | import { ArrowLeftOutlined } from "@ant-design/icons"; 4 | import { useNavigate } from "react-router-dom"; 5 | import "./BackButton.scss"; 6 | 7 | const { Link } = Typography; 8 | 9 | interface Props { 10 | title: String 11 | } 12 | 13 | export const BackButton = ({ title } : Props) => { 14 | const navigate = useNavigate(); 15 | 16 | const goBack = () => { 17 | navigate("/characters") 18 | } 19 | return ( 20 |
21 | goBack() }> {title} 22 |
23 | ) 24 | }; 25 | -------------------------------------------------------------------------------- /src/services/Common.ts: -------------------------------------------------------------------------------- 1 | export const Common = { 2 | 3 | /** 4 | * @description remove all undefined/null values from an object 5 | * @param obj 6 | */ 7 | clean({obj}: { obj: any }) { 8 | for (let propname in obj) { 9 | if ((obj[propname] === null || obj[propname] === undefined )) { 10 | delete obj[propname]; 11 | } 12 | } 13 | return obj 14 | }, 15 | 16 | /** 17 | * @description Get Filter values 18 | * @param string 19 | */ 20 | getFilterValues({string}: { string: any }) { 21 | // Convert string into array of integers 22 | return string && string.split(",").map(Number); 23 | }, 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "outDir": "./dist", 23 | "baseUrl": ".", 24 | "paths": { 25 | "src/*": ["./src/*"] 26 | } 27 | }, 28 | "include": [ 29 | "src" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/Layout/Layout.scss: -------------------------------------------------------------------------------- 1 | @import 'src/assets/variables/variables'; 2 | 3 | .layout { 4 | height: 100%; 5 | 6 | .header { 7 | display: flex; 8 | align-items: center; 9 | position: fixed; 10 | z-index: 1; 11 | width: 100%; 12 | background-color: $primary; 13 | .title { 14 | margin: 0; 15 | padding-left: 8px; 16 | color: $white; 17 | } 18 | } 19 | 20 | .content-wrapper { 21 | margin-top: 64px; 22 | padding: 20px; 23 | overflow-y: auto; 24 | .content { 25 | padding: 24px; 26 | //height: 100% 27 | } 28 | } 29 | 30 | 31 | .footer { 32 | text-align: center; 33 | color: $white; 34 | background-color: $primary; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/useFilters.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import { useNavigate, useLocation } from "react-router-dom"; 3 | 4 | export const useFilters = ({mapper}: { mapper: any }) => { 5 | const navigate = useNavigate(); 6 | const location = useLocation(); 7 | 8 | const query = useMemo(() => { 9 | return mapper.fromQueryStringToQuery(location.search); 10 | }, [location.search, mapper]); 11 | 12 | const onChangeQuery = useCallback( 13 | (newQuery) => { 14 | navigate({ 15 | pathname: location.pathname, 16 | search: mapper.fromQueryToQueryString({...query, ...newQuery}), 17 | }, {replace: true}); 18 | }, 19 | [navigate, location.pathname, mapper, query] 20 | ); 21 | 22 | return { 23 | query, 24 | onChangeQuery, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/models/character.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CharacterType { 2 | id?: number; // The id of the character. 3 | name: string; // The name of the character. 4 | status: string; // The status of the character ('Alive', 'Dead' or 'unknown'). 5 | species: string; // The species of the character. 6 | gender: string; // The gender of the character ('Female', 'Male', 'Genderless' or 'unknown'). 7 | origin: object; // Name and link to the character's origin location. 8 | location: object; // Name and link to the character's last known location endpoint. 9 | image: string; // Link to the character's image. All images are 300x300px and most are medium shots or portraits since they are intended to be used as avatars. 10 | episode: Array; // List of episodes in which this character appeared. 11 | url: string; // Link to the character's own URL endpoint. 12 | created: string; // Time at which the character was created in the database. 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rick-and-morty", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.2", 7 | "@testing-library/react": "^12.1.3", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.4.0", 10 | "@types/node": "^16.11.25", 11 | "@types/react": "^17.0.39", 12 | "@types/react-dom": "^17.0.11", 13 | "antd": "^4.18.8", 14 | "axios": "^0.26.0", 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2", 17 | "react-router-dom": "^6.2.1", 18 | "react-scripts": "5.0.0", 19 | "sass": "^1.49.8", 20 | "typescript": "^4.5.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/services/CharactersService.ts: -------------------------------------------------------------------------------- 1 | import { AxiosClient } from "src/api/AxiosClient"; 2 | import { AxiosResponse } from 'axios'; 3 | import { CharactersType } from 'src/models/characters.interface'; 4 | 5 | /** 6 | * @description Destructured axios and got the data response from its response object 7 | * @param response 8 | */ 9 | const responseBody = (response: AxiosResponse) => response.data; 10 | 11 | /** 12 | * @description Created a request object to handle GET and returned the destructured axios body created on line 9 13 | */ 14 | const requests = { 15 | get: (url: string, p: { params: Object } | undefined) => { 16 | return AxiosClient.get(url, p).then(responseBody) 17 | }, 18 | }; 19 | 20 | 21 | /** 22 | * @description Created and exported a character object that uses the request object created to handle GET operation using the request objects. 23 | */ 24 | export const Characters = { 25 | getCharacters: (data: any): Promise => requests.get('character', { params: data }), 26 | getCharacter: (id: any): Promise => requests.get(`character/${id}`, undefined), 27 | getCharacterLocation: (data: any): Promise => requests.get(`location/${data}`, undefined), 28 | getCharacterEpisodes: (data: any): Promise => requests.get(`episode/${data}`, undefined), 29 | }; 30 | -------------------------------------------------------------------------------- /src/domain/characters/Characters.mapper.ts: -------------------------------------------------------------------------------- 1 | import { Common } from "src/services/Common" 2 | import {CharactersQuery} from "src/domain/characters/CharactersQuery" 3 | import {QueryType} from "src/models/query.interface" 4 | import {PayloadAbstract} from "src/domain/PayloadAbstract" 5 | const { FILTER_KEYS } = CharactersQuery; 6 | export class CharactersMapper { 7 | 8 | /** 9 | * @description From Query => Query String 10 | * @param query 11 | */ 12 | static fromQueryToQueryString(query: Object) { 13 | // Remove all values that are undefined or null 14 | const queryData = Common.clean({obj: query}); 15 | return new URLSearchParams(queryData).toString(); 16 | } 17 | 18 | /** 19 | * @description From Query String => Query 20 | * @param qs 21 | */ 22 | static fromQueryStringToQuery(qs: string | undefined) { 23 | const query = new URLSearchParams(qs); 24 | const charactersQueryModel = new CharactersQuery(); 25 | charactersQueryModel.page = Number(query.get(FILTER_KEYS.PAGE)) || 1; 26 | charactersQueryModel.search = query.get(FILTER_KEYS.SEARCH) || undefined; 27 | return charactersQueryModel; 28 | } 29 | 30 | static fromQueryToPayload(query: QueryType) { 31 | const payload = new PayloadAbstract(); 32 | payload.page = query.page; 33 | return payload; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/characterCard/CharacterCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Card, Skeleton, Badge, Typography} from 'antd'; 3 | import {CharacterType} from "src/models/character.interface" 4 | import "./CharacterCard.scss" 5 | 6 | const {Title} = Typography; 7 | 8 | interface Props { 9 | data: CharacterType, 10 | isLoading: boolean, 11 | goToDetails: Function 12 | } 13 | 14 | 15 | export const CharacterCard = ({data, isLoading = false, goToDetails}: Props) => { 16 | const getStatusColor = (status: String) => { 17 | let statusColor = "" 18 | switch (status) { 19 | case "Dead": 20 | statusColor = "red"; 21 | break; 22 | case "Alive": 23 | statusColor = "green"; 24 | break; 25 | default: 26 | statusColor = "cyan" 27 | } 28 | return statusColor; 29 | } 30 | 31 | return ( 32 | <> 33 | {!isLoading ? 34 |
goToDetails(data.id)}> 35 | } 39 | > 40 | {data.name} 41 | - {data.species} 42 | 43 |
44 | : 45 | 46 | 47 | 48 | 49 | } 50 | 51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Rick & Morty 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/modules/Characters/Characters.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react' 2 | import "./Characters.scss"; 3 | import { List } from 'antd'; 4 | import { useCharacters } from "src/modules/Characters/provider/characters.provider" 5 | import {CharacterCard} from "src/components/characterCard/CharacterCard" 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | const Characters = () => { 9 | const { characters, paginationInfo, getCharacters, onChangeQuery, query, pendingData } = useCharacters(); 10 | let navigate = useNavigate(); 11 | // Get characters data 12 | useEffect( () => { 13 | getCharacters(); 14 | }, [query, getCharacters]) 15 | 16 | 17 | const changePagination = (page: number) => { 18 | onChangeQuery({ 19 | ...query, 20 | page: page 21 | }); 22 | } 23 | 24 | const goToDetails = (id: number) => navigate(`/characters/${id}`, {replace: true}); 25 | 26 | return ( 27 |
28 | ( 47 | 48 | 49 | 50 | )} 51 | />, 52 |
53 | ) 54 | } 55 | 56 | export default Characters 57 | -------------------------------------------------------------------------------- /src/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import "./Layout.scss"; 3 | import {BrowserRouter} from "react-router-dom"; 4 | import {Routes, Route, Navigate} from "react-router-dom"; 5 | import Characters from "src/modules/Characters/Characters" 6 | import Character from "src/modules/Character/Character" 7 | import {Layout, Typography} from 'antd'; 8 | import { CharactersProvider } from "src/modules/Characters/provider/characters.provider"; 9 | import { CharacterProvider } from "src/modules/Character/provider/character.provider"; 10 | 11 | const { Title } = Typography; 12 | const {Header, Content, Footer} = Layout; 13 | 14 | const DefaultLayout = () => { 15 | const logoPath = require("src/assets/img/logo.png"); 16 | return ( 17 | 18 | 19 |
20 | 21 | Rick and Morty 22 |
23 | 24 |
25 | 26 | {/*Characters*/} 27 | }/> 28 | {/*Character*/} 29 | }/> 30 | {/*No match route*/} 31 | } 34 | /> 35 | 36 |
37 |
38 |
39 | Rick and Morty ©2022 Created by somekindofwallflower 40 |
41 |
42 |
43 | ) 44 | } 45 | 46 | export default DefaultLayout 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rick and Morty React App :heavy_check_mark: 2 | 3 | Create a ReactJS app using the Rick & Morty [API](rickandmortyapi.com) 4 | * [X] Display the profiles of the characters (either with pagination or showing the first page only). 5 | * [X] The profile of a character should include: 6 |
    7 |
  • Image
  • 8 |
  • Character information (name, species, etc).
  • 9 |
  • Origin and location information (name, dimension, amount of residents, etc).
  • 10 |
  • Name of the chapters the character is featured on.
  • 11 |
12 | * [X] The API provides REST and GraphQL versions, for this exercise you should use the REST version. 13 | * [X] You are free to use any library/framework or even language. Be ready to explain the rationale for your choices. 14 | * [X] The exercise should be submitted in a public repository. 15 | * [X] Running your solution requires no global dependencies (besides node/npm/yarn) and it's possible to run it with only one command (besides yarn/npm install). 16 | * [X] Write the code with production standards in mind. 17 | 18 | 19 | Project [DEMO](https://rickandmortysomekindofwallflower.netlify.app/characters). 20 | 21 | ## Available Scripts 22 | 23 | In the project directory, you can run: 24 | 25 | ### `npm start` 26 | 27 | Runs the app in the development mode.\ 28 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 29 | 30 | The page will reload if you make edits.\ 31 | You will also see any lint errors in the console. 32 | 33 | ### `npm test` 34 | 35 | Launches the test runner in the interactive watch mode.\ 36 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 37 | 38 | ### `npm run build` 39 | 40 | Builds the app for production to the `build` folder.\ 41 | It correctly bundles React in production mode and optimizes the build for the best performance. 42 | 43 | The build is minified and the filenames include the hashes.\ 44 | Your app is ready to be deployed! 45 | 46 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 47 | 48 | ## Learn More 49 | 50 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 51 | 52 | To learn React, check out the [React documentation](https://reactjs.org/). 53 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/Characters/provider/characters.provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext, useCallback, useContext} from 'react'; 2 | import {Characters} from "src/services/CharactersService"; 3 | import {CharacterType} from "src/models/character.interface" 4 | import {PaginationType} from "src/models/pagination.interface"; 5 | import {QueryType} from "src/models/query.interface"; 6 | import { CharactersMapper } from "src/domain/characters/Characters.mapper" 7 | 8 | import {useFilters} from "src/hooks/useFilters" 9 | interface ICharactersContext { 10 | characters: CharacterType[] 11 | paginationInfo: PaginationType, 12 | query: QueryType, 13 | getCharacters: Function 14 | onChangeQuery: Function, 15 | pendingData: boolean 16 | } 17 | 18 | const defaultState = { 19 | characters: [], 20 | getCharacters: () => [], 21 | paginationInfo: { 22 | count: 0, 23 | next: null, 24 | pages: 1, 25 | prev: null, 26 | }, 27 | query: { 28 | page: 1, 29 | search: null 30 | }, 31 | onChangeQuery: () => {}, 32 | pendingData: false 33 | }; 34 | 35 | export const CharactersContext = createContext(defaultState); 36 | 37 | const useProvideCharacters = () => { 38 | const [pendingData, setPendingData] = useState(false); 39 | const [characters, setCharacters] = useState([]) 40 | const [paginationInfo, setPaginationInfo] = useState(defaultState.paginationInfo) 41 | const {query, onChangeQuery} = useFilters({mapper: CharactersMapper}) 42 | /** 43 | * @description Get Characters data 44 | */ 45 | const getCharacters = useCallback(async () => { 46 | try { 47 | setPendingData(true) 48 | const data = await Characters.getCharacters(CharactersMapper.fromQueryToPayload(query)); 49 | setCharacters(data.results); 50 | setPaginationInfo(data.info); 51 | return Promise.resolve(); 52 | } catch (e) { 53 | //TODO Handle error 54 | return Promise.reject(e); 55 | } finally { 56 | setPendingData(false); 57 | } 58 | }, [query]); 59 | 60 | return { 61 | characters, 62 | paginationInfo, 63 | query, 64 | getCharacters, 65 | onChangeQuery, 66 | pendingData 67 | }; 68 | 69 | } 70 | 71 | 72 | export const CharactersProvider = ({ children } : any) => { 73 | const characters = useProvideCharacters(); 74 | return ( 75 | 76 | {children} 77 | 78 | ); 79 | }; 80 | 81 | 82 | export const useCharacters = () => useContext(CharactersContext); 83 | -------------------------------------------------------------------------------- /src/modules/Character/provider/character.provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext, useCallback, useContext} from 'react'; 2 | import {Characters} from "src/services/CharactersService"; 3 | import {CharacterType} from "src/models/character.interface" 4 | import { useParams } from "react-router-dom"; 5 | 6 | interface ICharacterContext { 7 | character: CharacterType|any 8 | characterEpisodes: any 9 | characterLocation: any 10 | getCharacter: Function, 11 | pendingData: boolean 12 | } 13 | 14 | const defaultState = { 15 | character: {}, 16 | characterEpisodes: [], 17 | characterLocation: {}, 18 | getCharacter: () => {}, 19 | pendingData: false 20 | }; 21 | 22 | export const CharacterContext = createContext(defaultState); 23 | 24 | const useProvideCharacter = () => { 25 | const [pendingData, setPendingData] = useState(false); 26 | const [character, setCharacter] = useState({}) 27 | const [characterLocation, setCharacterLocation] = useState({}) 28 | const [characterEpisodes, setCharacterEpisodes] = useState([]) 29 | const params = useParams(); 30 | /** 31 | * @description Get Characters data 32 | */ 33 | const getCharacter = useCallback(async () => { 34 | 35 | try { 36 | setPendingData(true); 37 | const character:any = await Characters.getCharacter(params.id); 38 | setCharacter(character); 39 | const characterLocation = await Characters.getCharacterLocation(getIds([character.location?.url])) 40 | setCharacterLocation(characterLocation); 41 | const characterEpisodes = await Characters.getCharacterEpisodes(getIds(character.episode)); 42 | setCharacterEpisodes(Array.isArray(characterEpisodes) ? characterEpisodes : [characterEpisodes]); 43 | return Promise.resolve(); 44 | } catch (e) { 45 | //TODO Display an popup error message in case of error 46 | return Promise.reject(e); 47 | } finally { 48 | setPendingData(false); 49 | } 50 | }, [params.id]); 51 | 52 | const getIds = (urls: any) => { 53 | let ids: Array = []; 54 | urls?.forEach((episode: any)=> ids.push(parseInt(episode.match(/\d+/g)[0]))); 55 | return ids; 56 | } 57 | return { 58 | character, 59 | characterLocation, 60 | characterEpisodes, 61 | getCharacter, 62 | pendingData 63 | }; 64 | 65 | } 66 | 67 | 68 | export const CharacterProvider = ({ children } : any) => { 69 | const character = useProvideCharacter(); 70 | return ( 71 | 72 | {children} 73 | 74 | ); 75 | }; 76 | 77 | 78 | export const useCharacter = () => useContext(CharacterContext); 79 | -------------------------------------------------------------------------------- /src/modules/Character/Character.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {useCharacter} from "src/modules/Character/provider/character.provider"; 3 | import { BackButton } from "src/components/backButton/BackButton"; 4 | import {Card, Row, Col, Badge, Typography, Tag, Skeleton} from 'antd'; 5 | import "./Character.scss" 6 | 7 | const {Title, Text} = Typography; 8 | 9 | const Character = () => { 10 | const { character, getCharacter, characterLocation, characterEpisodes, pendingData} = useCharacter(); 11 | 12 | // Get characters data 13 | useEffect( () => { 14 | getCharacter(); 15 | }, [getCharacter]) 16 | 17 | const getStatusColor = (status: String) => { 18 | let statusColor = "" 19 | switch (status) { 20 | case "Dead": 21 | statusColor = "red"; 22 | break; 23 | case "Alive": 24 | statusColor = "green"; 25 | break; 26 | default: 27 | statusColor = "cyan" 28 | } 29 | return statusColor; 30 | } 31 | 32 | return( 33 |
34 | 35 | 36 | 37 | { 38 | pendingData ? : 39 | 40 | 41 | 42 | {character.name} 43 | 44 | 45 | {character.name} - {character.species} 46 | First seen in: <Text type="secondary">{character.origin?.name}</Text> 47 | Last known Location: <Text type="secondary">{characterLocation.name}</Text> 48 | Location Type: <Text type="secondary">{characterLocation.type}</Text> 49 | Dimensions: <Text type="secondary">{characterLocation.dimension}</Text> 50 | Residents: <Text type="secondary">{characterLocation.residents?.length - 1}</Text> 51 | 52 | 53 | 54 | 55 | Episodes 56 | {characterEpisodes.map((episode:any) => ( 57 | {episode.name} 58 | ))} 59 | 60 | 61 | 62 | 63 | } 64 | 65 | 66 |
67 | ) 68 | 69 | } 70 | 71 | 72 | export default Character; 73 | --------------------------------------------------------------------------------