├── 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