From 0a7bf7eb6128cd96b0b62f9a9a5f89382eb8bf8a Mon Sep 17 00:00:00 2001 From: Katia Duarte Date: Sun, 18 Apr 2021 22:07:18 +0100 Subject: [PATCH] feat: add modal and service to get episode information --- src/App.tsx | 6 ++ .../CharacterList/CharacterList.style.ts | 8 +- .../CharacterList/CharacterList.tsx | 18 +++- src/components/Modal/Modal.style.tsx | 48 ++++++++++ src/components/Modal/Modal.tsx | 96 +++++++++++++++++++ src/interfaces/episode-information.ts | 9 ++ src/lib/api-provider.ts | 19 +++- src/store/characters/action.ts | 5 + src/store/characters/reducer.test.ts | 12 +-- src/store/characters/reducer.ts | 22 +++-- src/store/characters/type.ts | 28 ++++-- src/store/store.ts | 4 +- src/utils/.gitkeep | 0 13 files changed, 239 insertions(+), 36 deletions(-) create mode 100644 src/components/Modal/Modal.style.tsx create mode 100644 src/components/Modal/Modal.tsx create mode 100644 src/interfaces/episode-information.ts delete mode 100644 src/utils/.gitkeep diff --git a/src/App.tsx b/src/App.tsx index 6322cb6..cbd85fa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,13 @@ import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; import CharacterList from './components/CharacterList/CharacterList'; +import Modal from './components/Modal/Modal'; import Search from './components/Search/Search'; import ApiProvider from './lib/api-provider'; +import { GlobalState } from './store/store'; const App = () => { + const isOpen = useSelector((state: GlobalState) => state.charactersState).modalStatus; const { getCharacters } = ApiProvider(); useEffect(() => { @@ -12,6 +16,8 @@ const App = () => { return ( <> + {isOpen ? : null} +
{/* */} diff --git a/src/components/CharacterList/CharacterList.style.ts b/src/components/CharacterList/CharacterList.style.ts index 2490c66..fb31683 100644 --- a/src/components/CharacterList/CharacterList.style.ts +++ b/src/components/CharacterList/CharacterList.style.ts @@ -12,7 +12,7 @@ export const CharacterListContainer = styled.section` display: flex; height: 174px; justify-content: center; - + cursor: pointer; img { align-items: center; display: flex; @@ -31,21 +31,21 @@ export const CharacterListContainer = styled.section` .species, .status { color: #222; - font-size: 14rem; + font-size: 12rem; font-weight: bold; margin: 8rem 0; opacity: 0.7; text-transform: uppercase; } - h1 { + h4 { font-size: 16rem; font-weight: bold; } .description { color: #222; - font-size: 12rem; + font-size: 11rem; margin-top: 8rem; br { diff --git a/src/components/CharacterList/CharacterList.tsx b/src/components/CharacterList/CharacterList.tsx index 35ea73f..8582eae 100644 --- a/src/components/CharacterList/CharacterList.tsx +++ b/src/components/CharacterList/CharacterList.tsx @@ -1,13 +1,16 @@ import React, { memo, useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Character } from '../../interfaces/character'; import { Info } from '../../interfaces/info'; +import { modalStatus, selectCharacter } from '../../store/characters/action'; import { GlobalState } from '../../store/store'; +import Modal from '../Modal/Modal'; import Pagination from '../Pagination/Pagination'; import { CharacterListContainer } from './CharacterList.style'; const CharacterList = (): JSX.Element => { - const store = useSelector((state: GlobalState) => state.charactersState); + const store = useSelector((state: GlobalState) => state.charactersState).response; + const dispatch = useDispatch(); const [list, setList] = useState([]); const [pagination, setPagination] = useState(); @@ -32,16 +35,21 @@ const CharacterList = (): JSX.Element => { return color; }; + const handleClick = (id: number): void => { + dispatch(selectCharacter(id)); + dispatch(modalStatus(true)); + }; + return ( {list.map((item: Character, i: number) => { return ( -
+
handleClick(item.id)}> {item.name}

{item.species}

-

{item.name}

+

{item.name}

{item.status} @@ -55,7 +63,7 @@ const CharacterList = (): JSX.Element => {

); })} - {pagination !== undefined && } + {/* {pagination !== undefined && } */} ); }; diff --git a/src/components/Modal/Modal.style.tsx b/src/components/Modal/Modal.style.tsx new file mode 100644 index 0000000..346231c --- /dev/null +++ b/src/components/Modal/Modal.style.tsx @@ -0,0 +1,48 @@ +import styled from 'styled-components'; + +export const ModalContainer = styled.div` + position: fixed; + z-index: 1; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + cursor: default; + + .content { + background-color: white; + position: absolute; + top: 20%; + left: 19%; + width: 60%; + padding: 20rem; + border-radius: 5rem; + + .close { + cursor: pointer; + display: flex; + justify-content: flex-end; + } + .profile { + align-items: center; + display: flex; + justify-content: center; + img { + border-radius: 50%; + flex: 1 1; + margin-right: 15rem; + } + + .profile-details { + flex: 2 1; + + h1 { + font-weight: bold; + color: green; + font-size: 26rem; + margin-bottom: 25rem; + text-transform: uppercase; + } + } + } + } +`; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx new file mode 100644 index 0000000..84d4d4b --- /dev/null +++ b/src/components/Modal/Modal.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from 'react'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ModalContainer } from './Modal.style'; +import { useDispatch, useSelector } from 'react-redux'; +import { modalStatus, selectCharacter } from '../../store/characters/action'; +import { GlobalState } from '../../store/store'; +import { Character } from '../../interfaces/character'; +import ApiProvider from '../../lib/api-provider'; +import { EpisodeInformation } from '../../interfaces/episode-information'; + +const Modal = (): JSX.Element => { + const dispatch = useDispatch(); + const { getEpisodeInformation } = ApiProvider(); + const [character, setCharacter] = useState(); + const [episode, setEpisode] = useState(); + const store = useSelector((state: GlobalState) => state.charactersState); + + useEffect(() => { + const found = store.response.results.find((i) => i.id === store.selected); + + if (found) { + if (found.episode.length > 0) { + getEpisodeInformation(found.episode[0]).then((res) => { + setEpisode(res); + }); + } + setCharacter(found); + } + }, [store.selected]); + + const handleClick = (): void => { + dispatch(modalStatus(false)); + dispatch(selectCharacter(0)); + }; + + const renderEpisodeInformation = (): JSX.Element => { + return ( + <> +
  • + First episode: {episode?.name} +
  • +
  • + Episode Number: {episode?.episode} +
  • +
  • + Air date: {episode?.air_date} +
  • + + ); + }; + + return ( + +
    + handleClick()}> + + + {character !== undefined && ( +
    + {character.name} +
    +

    Character Details

    +
      +
    • + Name: {character.name} +
    • +
    • + Status: {character.status} +
    • +
    • + Gender: {character.gender} +
    • +
    • + Species: {character.species} +
    • +
    • + Location: {character.location?.name} +
    • +
    • + Originally From: {character.origin?.name} +
    • +
    • + Episodes Count: {character.episode.length} +
    • + {episode !== undefined && renderEpisodeInformation()} +
    +
    +
    + )} +
    +
    + ); +}; + +export default Modal; diff --git a/src/interfaces/episode-information.ts b/src/interfaces/episode-information.ts new file mode 100644 index 0000000..e67faea --- /dev/null +++ b/src/interfaces/episode-information.ts @@ -0,0 +1,9 @@ +export type EpisodeInformation = { + id: number; + name: string; + air_date: string; + episode: string; + characters: string[]; + url: string; + created: string; +}; diff --git a/src/lib/api-provider.ts b/src/lib/api-provider.ts index 9dc4014..ba018b1 100644 --- a/src/lib/api-provider.ts +++ b/src/lib/api-provider.ts @@ -1,19 +1,21 @@ import axios, { AxiosResponse } from 'axios'; import { useDispatch } from 'react-redux'; import { CharacterResponse } from '../interfaces/character-response'; +import { EpisodeInformation } from '../interfaces/episode-information'; import { updateData } from '../store/characters/action'; const ApiProvider = () => { const API_KEY = process.env.REACT_APP_API || ''; const dispatch = useDispatch(); - const getCharacters = async (searchTerm = ''): Promise => { + const getCharacters = async (searchTerm = '', page = 1): Promise => { console.log('updating search', searchTerm); - const response = await axios + await axios .get(`${API_KEY}/character/`, { params: { name: searchTerm, + page: page, }, }) .then((res: AxiosResponse) => { @@ -24,12 +26,25 @@ const ApiProvider = () => { .catch((error) => { console.log(error); }); + }; + + const getEpisodeInformation = async (url: string): Promise => { + const response = await axios + .get(`${url}`) + .then((res: AxiosResponse) => { + console.log(`Status: ${res.status}`); + return res.data; + }) + .catch((error) => { + console.log(error); + }); return response; }; return { getCharacters, + getEpisodeInformation, }; }; diff --git a/src/store/characters/action.ts b/src/store/characters/action.ts index 6df600c..8c527b4 100644 --- a/src/store/characters/action.ts +++ b/src/store/characters/action.ts @@ -4,3 +4,8 @@ import { CharactersType } from './type'; export const updateData = (result: CharacterResponse): PayloadAction => action(CharactersType.UPDATE, result); + +export const modalStatus = (status: boolean): PayloadAction => + action(CharactersType.MODAL_STATUS, status); + +export const selectCharacter = (id: number): PayloadAction => action(CharactersType.SELECTED, id); diff --git a/src/store/characters/reducer.test.ts b/src/store/characters/reducer.test.ts index 82871f5..a5bb695 100644 --- a/src/store/characters/reducer.test.ts +++ b/src/store/characters/reducer.test.ts @@ -25,8 +25,8 @@ describe('Test Suite for Characters Store Reducer', () => { payload: mock, }; - expect(charactersReducer(INITIAL_STATE, updateAction).results.length).toEqual(0); - expect(charactersReducer(INITIAL_STATE, updateAction).info.nextPage).toEqual(1); + expect(charactersReducer(INITIAL_STATE, updateAction).response.results.length).toEqual(0); + expect(charactersReducer(INITIAL_STATE, updateAction).response.info.nextPage).toEqual(1); }); it('should update store by adding current characters list, and return 1 for next page if next does not contain page', () => { @@ -48,8 +48,8 @@ describe('Test Suite for Characters Store Reducer', () => { payload: mock, }; - expect(charactersReducer(INITIAL_STATE, updateAction).results.length).toEqual(0); - expect(charactersReducer(INITIAL_STATE, updateAction).info.nextPage).toEqual(1); + expect(charactersReducer(INITIAL_STATE, updateAction).response.results.length).toEqual(0); + expect(charactersReducer(INITIAL_STATE, updateAction).response.info.nextPage).toEqual(1); }); it('should update store by adding current characters list, and return 3 for next page', () => { @@ -71,7 +71,7 @@ describe('Test Suite for Characters Store Reducer', () => { payload: mock, }; - expect(charactersReducer(INITIAL_STATE, updateAction).results.length).toEqual(0); - expect(charactersReducer(INITIAL_STATE, updateAction).info.nextPage).toEqual(3); + expect(charactersReducer(INITIAL_STATE, updateAction).response.results.length).toEqual(0); + expect(charactersReducer(INITIAL_STATE, updateAction).response.info.nextPage).toEqual(3); }); }); diff --git a/src/store/characters/reducer.ts b/src/store/characters/reducer.ts index a3ed122..6600b0b 100644 --- a/src/store/characters/reducer.ts +++ b/src/store/characters/reducer.ts @@ -1,6 +1,5 @@ import { Reducer } from 'redux'; -import { CharacterResponse } from '../../interfaces/character-response'; -import { CharactersType, INITIAL_STATE } from './type'; +import { CharacterStore, CharactersType, INITIAL_STATE } from './type'; type CharactersReducer = { type: string; @@ -19,21 +18,26 @@ const getNextPage = (nextUrl: string | null): number => { return +split[1]; }; -const charactersReducer: Reducer = ( - state: CharacterResponse = INITIAL_STATE, +const charactersReducer: Reducer = ( + state: CharacterStore = INITIAL_STATE, action: CharactersReducer, ) => { switch (action.type) { case CharactersType.UPDATE: return { ...state, - results: action.payload.results, - info: { - ...action.payload.info, - nextPage: getNextPage(action.payload.info.next), + response: { + results: action.payload.results, + info: { + ...action.payload.info, + nextPage: getNextPage(action.payload.info.next), + }, }, }; - + case CharactersType.MODAL_STATUS: + return { ...state, modalStatus: action.payload }; + case CharactersType.SELECTED: + return { ...state, selected: action.payload }; default: return { ...state }; } diff --git a/src/store/characters/type.ts b/src/store/characters/type.ts index 656e969..c04e815 100644 --- a/src/store/characters/type.ts +++ b/src/store/characters/type.ts @@ -2,15 +2,27 @@ import { CharacterResponse } from '../../interfaces/character-response'; export const CharactersType = { UPDATE: '@@CHARACTERS/UPDATE_DATA', + MODAL_STATUS: '@@CHARACTERS/MODAL_STATUS', + SELECTED: '@@CHARACTERS/SELECTED', }; -export const INITIAL_STATE: CharacterResponse = { - info: { - count: 0, - next: null, - pages: 0, - prev: null, - nextPage: 1, +export interface CharacterStore { + response: CharacterResponse; + modalStatus: boolean; + selected: number; +} + +export const INITIAL_STATE: CharacterStore = { + response: { + info: { + count: 0, + next: null, + pages: 0, + prev: null, + nextPage: 1, + }, + results: [], }, - results: [], + modalStatus: false, + selected: 0, }; diff --git a/src/store/store.ts b/src/store/store.ts index 51622de..88d8b7a 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,10 +1,10 @@ import { combineReducers, applyMiddleware, createStore } from '@reduxjs/toolkit'; import thunk from 'redux-thunk'; -import { CharacterResponse } from '../interfaces/character-response'; import charactersReducer from './characters/reducer'; +import { CharacterStore } from './characters/type'; export interface GlobalState { - charactersState: CharacterResponse; + charactersState: CharacterStore; } const combinedReducer = combineReducers({ diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep deleted file mode 100644 index e69de29..0000000