Compare commits
20 Commits
ec0af2f3ac
...
6acabeb91a
Author | SHA1 | Date |
---|---|---|
Jason Zhu | 6acabeb91a | |
Jason Zhu | ff92db699d | |
Jason Zhu | f5e0a75c64 | |
Jason Zhu | 749a745b4a | |
Jason Zhu | 0c4f2d4b67 | |
Jason Zhu | 2ae23cdbfa | |
Jason Zhu | 1f0ab75d6e | |
Jason Zhu | 93fe0cb24a | |
Jason Zhu | 40a64e9033 | |
Jason Zhu | a21427b91f | |
Jason Zhu | be9d767bfd | |
Jason Zhu | 6295a6a792 | |
Jason Zhu | dbb7ab99d2 | |
Jason Zhu | 92d3428c84 | |
Jason Zhu | ce3d3ce0d7 | |
Jason Zhu | 5068a6f728 | |
Jason Zhu | b56eae4e40 | |
Jason Zhu | b2839de8a7 | |
Jason Zhu | 133884e717 | |
Jason Zhu | bb5f6d73c4 |
|
@ -1,3 +1,5 @@
|
||||||
|
import 'index.css';
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
controls: {
|
controls: {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import Header from 'components/Header';
|
import Header from 'components/Header';
|
||||||
import Pokedex from './features/Pokedex';
|
import Pokedex from 'features/Pokedex';
|
||||||
|
import Filters from 'features/Filters';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App app_container">
|
<div className="App app_container">
|
||||||
<Header />
|
<Header />
|
||||||
|
<Filters />
|
||||||
<Pokedex />
|
<Pokedex />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { listenerMiddleware } from './listenerMiddleware';
|
import { listenerMiddleware } from './listenerMiddleware';
|
||||||
import { pokedexApi } from 'features/Pokedex/pokedexApi';
|
|
||||||
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
|
import { pokedexSlice } from 'features/Pokedex/pokedexSlice';
|
||||||
|
import { filterSlice } from 'features/Filters/filterSlice';
|
||||||
|
import { filterApi } from 'features/Filters/filterApi';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
// component slices
|
// component slices
|
||||||
pokedex: pokedexSlice.reducer,
|
pokedex: pokedexSlice.reducer,
|
||||||
|
filter: filterSlice.reducer,
|
||||||
|
|
||||||
// api
|
// api slices
|
||||||
[pokedexApi.reducerPath]: pokedexApi.reducer,
|
[filterApi.reducerPath]: filterApi.reducer,
|
||||||
},
|
},
|
||||||
middleware: getDefaultMiddleware =>
|
middleware: getDefaultMiddleware =>
|
||||||
getDefaultMiddleware().concat(
|
getDefaultMiddleware().concat(
|
||||||
pokedexApi.middleware,
|
filterApi.middleware,
|
||||||
listenerMiddleware.middleware,
|
listenerMiddleware.middleware,
|
||||||
),
|
),
|
||||||
devTools: true,
|
devTools: true,
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,7 @@
|
||||||
|
.fa-mars {
|
||||||
|
color: #7070ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-venus {
|
||||||
|
color: rgb(224, 61, 88);
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import GenderRate, { GenderRateProps } from './GenderRate';
|
||||||
|
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Component/GenderRate',
|
||||||
|
component: GenderRate,
|
||||||
|
} as ComponentMeta<typeof GenderRate>;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof GenderRate> = (args: GenderRateProps) => (
|
||||||
|
<GenderRate {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
|
||||||
|
Primary.args = {
|
||||||
|
genderRatio: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Option1 = Template.bind({});
|
||||||
|
Option1.args = {
|
||||||
|
genderRatio: 1,
|
||||||
|
};
|
|
@ -0,0 +1,122 @@
|
||||||
|
import './GenderRate.css';
|
||||||
|
|
||||||
|
export interface GenderRateProps {
|
||||||
|
genderRatio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenderRate = ({ genderRatio }: GenderRateProps) => {
|
||||||
|
switch (genderRatio) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="gender-male">
|
||||||
|
100% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
0% <i className="fa fa-venus">♀</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
87.5% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
12.5% <i className="fa fa-venus">♀</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
75% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
25% <i className="fa fa-venus">♀</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
62.5% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
37.5% <i className="fa fa-venus">♀</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 4:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
50% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
50% <i className="fa fa-venus">♀</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 5:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
37.5% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
62.5% <i className="fa fa-venus">♀</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 6:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
25% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
75% <i className="fa fa-venus">♀</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 7:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
12.5% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
87.5% <i className="fa fa-venus"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 8:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
0% <i className="fa fa-mars">♂</i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
100% <i className="fa fa-venus"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <span>Loading...</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenderRate;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './GenderRate';
|
|
@ -28,7 +28,7 @@ html {
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Press Start 2P';
|
font-family: 'Press Start 2P';
|
||||||
src: url('../../../assets/PressStart2P-Regular.ttf') format('truetype');
|
src: url('assets/PressStart2P-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail__container {
|
.thumbnail__container {
|
||||||
|
@ -40,7 +40,7 @@ html {
|
||||||
margin: 2rem;
|
margin: 2rem;
|
||||||
/* border: 15px solid var(--cardborder); */
|
/* border: 15px solid var(--cardborder); */
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
min-width: 220px;
|
width: 220px;
|
||||||
height: 285px;
|
height: 285px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* box-shadow: 0 5px 25px 1px rgb(0 0 0 / 50%); */
|
/* box-shadow: 0 5px 25px 1px rgb(0 0 0 / 50%); */
|
||||||
|
@ -96,6 +96,30 @@ h3 {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poke__type {
|
||||||
|
display: flex;
|
||||||
|
grid-gap: 0 10px;
|
||||||
|
gap: 0 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poke__type__bg > img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poke__type__bg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.grass {
|
.grass {
|
||||||
background: var(--grass);
|
background: var(--grass);
|
||||||
box-shadow: 0 0 20px var(--grass);
|
box-shadow: 0 0 20px var(--grass);
|
|
@ -8,7 +8,7 @@ import charizard_svg from './assets/stories/charizard.svg';
|
||||||
import charizard_info from './assets/stories/charizard.json';
|
import charizard_info from './assets/stories/charizard.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Pokedex/PokemonCard',
|
title: 'Component/PokemonCard',
|
||||||
component: PokemonCard,
|
component: PokemonCard,
|
||||||
} as ComponentMeta<typeof PokemonCard>;
|
} as ComponentMeta<typeof PokemonCard>;
|
||||||
|
|
|
@ -17,12 +17,7 @@ export function formatNumber(num: number) {
|
||||||
return '#' + num.toString().padStart(3, '0');
|
return '#' + num.toString().padStart(3, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PokemonCard({
|
const PokemonCard = ({ id, name, image, types }: PokemonCardProps) => {
|
||||||
id,
|
|
||||||
name,
|
|
||||||
image,
|
|
||||||
types,
|
|
||||||
}: PokemonCardProps) {
|
|
||||||
let finalColor;
|
let finalColor;
|
||||||
|
|
||||||
if (types.length === 2) {
|
if (types.length === 2) {
|
||||||
|
@ -67,8 +62,12 @@ export default function PokemonCard({
|
||||||
</div>
|
</div>
|
||||||
<div className="poke__name">
|
<div className="poke__name">
|
||||||
<h3>{name}</h3>
|
<h3>{name}</h3>
|
||||||
<PokemonTypes types={types} />
|
<div className="poke__type">
|
||||||
|
<PokemonTypes types={types} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default PokemonCard;
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
@ -5,7 +5,7 @@ import { Tooltip, Zoom } from '@mui/material';
|
||||||
import * as pokeTypeAsset from 'assets/types';
|
import * as pokeTypeAsset from 'assets/types';
|
||||||
import './PokemonTypes.css';
|
import './PokemonTypes.css';
|
||||||
|
|
||||||
function findPokeTypeAsset(pokeType: string) {
|
export const findPokeTypeAsset = (pokeType: string) => {
|
||||||
switch (pokeType) {
|
switch (pokeType) {
|
||||||
case 'normal':
|
case 'normal':
|
||||||
return pokeTypeAsset.pokeType_normal;
|
return pokeTypeAsset.pokeType_normal;
|
||||||
|
@ -46,7 +46,7 @@ function findPokeTypeAsset(pokeType: string) {
|
||||||
default:
|
default:
|
||||||
return pokeTypeAsset.pokeType_normal;
|
return pokeTypeAsset.pokeType_normal;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface PokemonTypesProps {
|
export interface PokemonTypesProps {
|
||||||
types: string[];
|
types: string[];
|
||||||
|
@ -54,7 +54,8 @@ export interface PokemonTypesProps {
|
||||||
|
|
||||||
const PokemonTypes = ({ types }: PokemonTypesProps) => {
|
const PokemonTypes = ({ types }: PokemonTypesProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="poke__type">
|
// css is set in consumer
|
||||||
|
<>
|
||||||
{types.map(type => (
|
{types.map(type => (
|
||||||
<Tooltip title={type} key={type} TransitionComponent={Zoom} arrow>
|
<Tooltip title={type} key={type} TransitionComponent={Zoom} arrow>
|
||||||
<div className={`poke__type__bg ${type}`}>
|
<div className={`poke__type__bg ${type}`}>
|
||||||
|
@ -62,7 +63,7 @@ const PokemonTypes = ({ types }: PokemonTypesProps) => {
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './PokemonTypes';
|
||||||
|
export { default } from './PokemonTypes';
|
|
@ -0,0 +1,42 @@
|
||||||
|
html {
|
||||||
|
--toggle: hsl(230deg 17% 85%);
|
||||||
|
--bgcolor: none;
|
||||||
|
--colorPrimary: #000;
|
||||||
|
--filterHeading: #000;
|
||||||
|
--selectBg: #fff;
|
||||||
|
--selectText: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'barcadebrawl';
|
||||||
|
src: url('assets/fonts/BarcadeBrawl.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 7vh 0 5vh 0;
|
||||||
|
gap: 0 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'barcadebrawl';
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: Fix incorrect render of input */
|
||||||
|
.filter__items > input {
|
||||||
|
width: 10vw;
|
||||||
|
margin-top: 5px;
|
||||||
|
background-color: var(--selectBg);
|
||||||
|
color: var(--selectText);
|
||||||
|
border: 1px solid var(--selectText);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__items > div,
|
||||||
|
label {
|
||||||
|
color: var(--filterHeading);
|
||||||
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useGetTypeListQuery } from 'features/Pokedex/pokedexApi';
|
import { useAppDispatch, useAppSelector } from 'app/hooks';
|
||||||
|
import { fetchPokemonsInTheRegion } from 'features/Pokedex/pokedexSlice';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setSelectedRegion,
|
setSelectedRegion,
|
||||||
setSelectedType,
|
setSelectedType,
|
||||||
setSelectedSort,
|
setSelectedSort,
|
||||||
fetchPokemonsInTheRegion,
|
|
||||||
setRegionOptions,
|
setRegionOptions,
|
||||||
setSortOptions,
|
|
||||||
setTypeOptions,
|
setTypeOptions,
|
||||||
|
setSortOptions,
|
||||||
setSearchInput,
|
setSearchInput,
|
||||||
} from 'features/Pokedex/pokedexSlice';
|
} from './filterSlice';
|
||||||
import { RegionPokemonRange } from 'features/Pokedex/types/slice';
|
import { useGetTypeListQuery } from './filterApi';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/hooks';
|
import { RegionPokemonRange } from './types/slice';
|
||||||
|
import './Filters.css';
|
||||||
|
|
||||||
export const createRegionPokemonListOptionElements = (
|
export const createRegionPokemonListOptionElements = (
|
||||||
data: RegionPokemonRange[],
|
data: RegionPokemonRange[],
|
||||||
|
@ -54,13 +56,12 @@ const useGetSortOptions = () => {
|
||||||
|
|
||||||
const Filters = () => {
|
const Filters = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedRegion = useAppSelector(state => state.pokedex.selectedRegion);
|
const selectedRegion = useAppSelector(state => state.filter.selectedRegion);
|
||||||
const selectedType = useAppSelector(state => state.pokedex.selectedType);
|
const selectedType = useAppSelector(state => state.filter.selectedType);
|
||||||
const selectedSort = useAppSelector(state => state.pokedex.selectedSort);
|
const selectedSort = useAppSelector(state => state.filter.selectedSort);
|
||||||
|
const searchInput = useAppSelector(state => state.filter.searchInput);
|
||||||
|
|
||||||
const regionPokemonList = useAppSelector(
|
const regionPokemonList = useAppSelector(state => state.filter.regionOptions);
|
||||||
state => state.pokedex.regionOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: fetchedRegionOptions } = useGetRegionOptions();
|
const { data: fetchedRegionOptions } = useGetRegionOptions();
|
||||||
const { data: fetchedTypeOptions, isLoading: isFetchingTypeOptions } =
|
const { data: fetchedTypeOptions, isLoading: isFetchingTypeOptions } =
|
||||||
|
@ -89,7 +90,7 @@ const Filters = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="filter__container">
|
<div className="filter__container noselect">
|
||||||
<div className="filter__items">
|
<div className="filter__items">
|
||||||
<div>
|
<div>
|
||||||
<div>REGION</div>
|
<div>REGION</div>
|
||||||
|
@ -148,6 +149,7 @@ const Filters = () => {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
onChange={e => dispatch(setSearchInput(e.target.value))}
|
onChange={e => dispatch(setSearchInput(e.target.value))}
|
||||||
|
value={searchInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||||
|
import { pokeApiBaseQuery } from '../../services/pokeapi/paginationBaseQuery';
|
||||||
|
import {
|
||||||
|
RegionListResponseData,
|
||||||
|
TypeListResponseData,
|
||||||
|
} from '../Pokedex/types/api';
|
||||||
|
|
||||||
|
export const filterApi = createApi({
|
||||||
|
reducerPath: 'filterApi',
|
||||||
|
baseQuery: pokeApiBaseQuery,
|
||||||
|
endpoints: builder => ({
|
||||||
|
getTypeList: builder.query<TypeListResponseData, void>({
|
||||||
|
query: () => ({ url: 'type', fetchAllPages: true }),
|
||||||
|
transformResponse: (response: RegionListResponseData) => {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
results: [{ name: 'All Types', url: '' }, ...response.results],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useGetTypeListQuery } = filterApi;
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit';
|
||||||
|
import { FilterState } from './types/slice';
|
||||||
|
import { RegionPokemonRange } from './types/slice';
|
||||||
|
import { filterApi } from './filterApi';
|
||||||
|
|
||||||
|
filterApi.endpoints.getTypeList.initiate(); // initialize type list fetching
|
||||||
|
|
||||||
|
const initialState: FilterState = {
|
||||||
|
regionOptions: [],
|
||||||
|
typeOptions: [],
|
||||||
|
sortOptions: [],
|
||||||
|
selectedRegion: '',
|
||||||
|
selectedType: '',
|
||||||
|
selectedSort: '',
|
||||||
|
searchInput: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterSlice: Slice<FilterState> = createSlice({
|
||||||
|
name: 'filter',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSelectedRegion: (state, action: PayloadAction<string>) => {
|
||||||
|
state.selectedRegion = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedType: (state, action: PayloadAction<string>) => {
|
||||||
|
state.selectedType = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedSort: (state, action: PayloadAction<string>) => {
|
||||||
|
state.selectedSort = action.payload;
|
||||||
|
},
|
||||||
|
setRegionOptions: (state, action: PayloadAction<RegionPokemonRange[]>) => {
|
||||||
|
state.regionOptions = action.payload;
|
||||||
|
},
|
||||||
|
setTypeOptions: (state, action: PayloadAction<string[]>) => {
|
||||||
|
state.typeOptions = action.payload;
|
||||||
|
},
|
||||||
|
setSortOptions: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ name: string; value: string }[]>,
|
||||||
|
) => {
|
||||||
|
state.sortOptions = action.payload;
|
||||||
|
},
|
||||||
|
setSearchInput: (state, action: PayloadAction<string>) => {
|
||||||
|
state.searchInput = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: builder => {
|
||||||
|
builder.addMatcher(
|
||||||
|
filterApi.endpoints.getTypeList.matchFulfilled,
|
||||||
|
(state, action) => {
|
||||||
|
if (action.payload && action.payload.results.length > 0) {
|
||||||
|
const regionListResults = action.payload.results;
|
||||||
|
state.typeOptions = regionListResults.map(region => region.name);
|
||||||
|
|
||||||
|
state.selectedType = action.payload.results[0].name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setSelectedRegion,
|
||||||
|
setSelectedType,
|
||||||
|
setSelectedSort,
|
||||||
|
setRegionOptions,
|
||||||
|
setTypeOptions,
|
||||||
|
setSortOptions,
|
||||||
|
setSearchInput,
|
||||||
|
} = filterSlice.actions;
|
||||||
|
|
||||||
|
export default filterSlice.reducer;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './Filters';
|
|
@ -0,0 +1,15 @@
|
||||||
|
export type FilterState = {
|
||||||
|
regionOptions: RegionPokemonRange[];
|
||||||
|
typeOptions: string[];
|
||||||
|
sortOptions: { name: string; value: string }[];
|
||||||
|
selectedRegion: string;
|
||||||
|
selectedType: string;
|
||||||
|
selectedSort: string;
|
||||||
|
searchInput: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegionPokemonRange = {
|
||||||
|
region: string;
|
||||||
|
startId: number;
|
||||||
|
endId: number;
|
||||||
|
};
|
|
@ -0,0 +1,350 @@
|
||||||
|
html {
|
||||||
|
--pokename: #000;
|
||||||
|
--cardborder: #fff;
|
||||||
|
--pokenumber: hsl(228, 28%, 20%);
|
||||||
|
--info: #fff;
|
||||||
|
--bggradient: url('assets/bg.png');
|
||||||
|
--bgcolor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
--pokename: hsl(228, 28%, 20%);
|
||||||
|
--cardborder: hsl(228, 28%, 20%);
|
||||||
|
--pokenumber: hsl(228, 28%, 20%);
|
||||||
|
--info: hsl(228, 28%, 20%);
|
||||||
|
--bggradient: none;
|
||||||
|
--bgcolor: #16171f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__img {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: #ffffff78;
|
||||||
|
padding: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__img img {
|
||||||
|
width: 120px;
|
||||||
|
height: 147px;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
margin: 0.3rem;
|
||||||
|
border-radius: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon__name {
|
||||||
|
font-size: 22px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon__id {
|
||||||
|
color: var(--pokenumber);
|
||||||
|
font-family: 'Teko', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__data__header {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__data__type {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__data__type img {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__headings {
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 27px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__data__abilities {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__data__data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
grid-gap: 0px 15px;
|
||||||
|
gap: 0px 15px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0px 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: #ffffff40;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__stat__columns {
|
||||||
|
width: 135px;
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiDialog-paperFullWidth {
|
||||||
|
width: 63%;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 10px 20px rgba(175, 136, 136, 0.29),
|
||||||
|
0 6px 6px rgba(0, 0, 0, 0.43);
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiDialog-container {
|
||||||
|
background-color: var(--bgcolor);
|
||||||
|
background-image: var(--bggradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiBackdrop-root {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__data__dimensions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__container__stat__columns__name {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: crimson;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog__bg {
|
||||||
|
background-image: url('assets/bg.png');
|
||||||
|
background-repeat: repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog__content {
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon__name {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close__btn {
|
||||||
|
width: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right__box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
gap: 3px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evolution__box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evolution__sub__box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evolution__box .evo_img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .evolution__img__div:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
} */
|
||||||
|
|
||||||
|
.evolution__img__div {
|
||||||
|
height: 113px;
|
||||||
|
width: 113px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 5px 15px 4px rgb(0 0 0 / 30%);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.35s;
|
||||||
|
margin-top: 3vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evolution__poke__name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow__right {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiDialogContent-root:first-child {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability__list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0 30px;
|
||||||
|
padding-inline-start: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability__list__bg {
|
||||||
|
background-color: #ffffff40;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiTooltip-tooltip {
|
||||||
|
font-size: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
height: 80;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgb(77, 77, 77),
|
||||||
|
rgb(77, 77, 77)
|
||||||
|
);
|
||||||
|
width: 1%;
|
||||||
|
margin-left: 2vw;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transparency__div {
|
||||||
|
background: #ffffff40;
|
||||||
|
height: 113px;
|
||||||
|
width: 113px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
background-color: #ffffff40;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-mars {
|
||||||
|
color: #7070ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-venus {
|
||||||
|
color: rgb(224, 61, 88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dimensions {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon__genera {
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 1.5vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* phone */
|
||||||
|
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
.MuiDialog-paperFullWidth {
|
||||||
|
width: 75%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.info__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.info__container__img {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
.info__container__data {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
.right__box {
|
||||||
|
margin-top: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.info__container__data__data {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.info__container__stat__columns__name,
|
||||||
|
.info__container__stat__columns__val,
|
||||||
|
.stats,
|
||||||
|
.dimensions,
|
||||||
|
.ability {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.info__container__data__abilities {
|
||||||
|
margin-top: 20px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0px;
|
||||||
|
}
|
||||||
|
.right__box .desc {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.info__container__headings {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.evolution__box {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.evolution__sub__box {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.arrow__right {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.evolution__poke__name {
|
||||||
|
margin: 12px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import InfoDialog, { InfoDialogProps } from './InfoDialog';
|
||||||
|
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Features/InfoDialog',
|
||||||
|
component: InfoDialog,
|
||||||
|
} as ComponentMeta<typeof InfoDialog>;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof InfoDialog> = (args: InfoDialogProps) => (
|
||||||
|
<InfoDialog {...args} />
|
||||||
|
);
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { Dialog, DialogContent, Tooltip, Zoom } from '@mui/material';
|
||||||
|
|
||||||
|
import { findPokeTypeAsset } from 'components/PokemonTypes';
|
||||||
|
import { colorTypeGradients } from 'components/PokemonCard/utils';
|
||||||
|
import GenderRate from 'components/GenderRate';
|
||||||
|
|
||||||
|
export interface InfoDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
cancel: boolean;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
genere: string;
|
||||||
|
types: string[];
|
||||||
|
height: number;
|
||||||
|
weight: number;
|
||||||
|
genderRatio: number;
|
||||||
|
description: string;
|
||||||
|
abilities: string[];
|
||||||
|
stats: {
|
||||||
|
hp: number;
|
||||||
|
attack: number;
|
||||||
|
defense: number;
|
||||||
|
spAttack: number;
|
||||||
|
spDefense: number;
|
||||||
|
speed: number;
|
||||||
|
};
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfoDialog = (props: InfoDialogProps) => {
|
||||||
|
let finalColor;
|
||||||
|
|
||||||
|
if (props.types.length === 2) {
|
||||||
|
finalColor = colorTypeGradients(
|
||||||
|
props.types[0],
|
||||||
|
props.types[1],
|
||||||
|
props.types.length,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finalColor = colorTypeGradients(
|
||||||
|
props.types[0],
|
||||||
|
props.types[0],
|
||||||
|
props.types.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Dialog
|
||||||
|
aria-labelledby="customized-dialog-title"
|
||||||
|
open={props.open}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="md"
|
||||||
|
className="dialog__bg noselect"
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(${finalColor[0]}, ${finalColor[1]})`,
|
||||||
|
}}
|
||||||
|
className="dialog__content"
|
||||||
|
>
|
||||||
|
<div className="info__container">
|
||||||
|
<div className="info__container__img">
|
||||||
|
<div className="pokemon__id">
|
||||||
|
#{String(props.id).padStart(3, '0')}
|
||||||
|
</div>
|
||||||
|
<div className="pokemon__name">{props.name}</div>
|
||||||
|
<div
|
||||||
|
className="pokemon__genera"
|
||||||
|
style={{ background: finalColor[0] }}
|
||||||
|
>
|
||||||
|
{props.genere}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src={props.image} alt="poke-img" />
|
||||||
|
</div>
|
||||||
|
<div className="info__container__data__type">
|
||||||
|
{props.types.map(type => (
|
||||||
|
<Tooltip
|
||||||
|
title={type}
|
||||||
|
key={type}
|
||||||
|
TransitionComponent={Zoom}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<div className={`poke__type__bg ${type}`}>
|
||||||
|
<img src={findPokeTypeAsset(type)} alt="poke-type" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="dimensions">
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
className="info__container__headings"
|
||||||
|
style={{ fontSize: '20px' }}
|
||||||
|
>
|
||||||
|
Height
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
className="info__container__headings"
|
||||||
|
style={{ fontSize: '20px' }}
|
||||||
|
>
|
||||||
|
Weight
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="gender__container">
|
||||||
|
{props.genderRatio === -1 ? (
|
||||||
|
'Genderless'
|
||||||
|
) : (
|
||||||
|
<GenderRate genderRatio={props.genderRatio} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="info__container__data">
|
||||||
|
<div className={'right__box'}>
|
||||||
|
<div>
|
||||||
|
<div className={'info__container__headings'}>About</div>
|
||||||
|
<div className={'desc'}>{props.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoDialog;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './InfoDialog';
|
||||||
|
export { default } from './InfoDialog';
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from './Filters';
|
|
||||||
export { default as useFilterLoaded } from './useFilterLoaded';
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useGetRegionListQuery, useGetTypeListQuery } from '../pokedexApi';
|
|
||||||
|
|
||||||
const useFilterLoaded = () => {
|
|
||||||
const { isLoading: isLoadingRegionList } = useGetRegionListQuery();
|
|
||||||
const { isLoading: isLoadingTypeList } = useGetTypeListQuery();
|
|
||||||
const [isFilterLoaded, setIsFilterLoaded] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoadingRegionList && !isLoadingTypeList) {
|
|
||||||
setIsFilterLoaded(true);
|
|
||||||
}
|
|
||||||
}, [isLoadingRegionList, isLoadingTypeList]);
|
|
||||||
|
|
||||||
return isFilterLoaded;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFilterLoaded;
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PokemonCard, { PokemonCardProps } from './PokemonCard';
|
import PokemonCard, { PokemonCardProps } from 'components/PokemonCard';
|
||||||
import Filters from './Filters';
|
|
||||||
import Loading from 'components/Loading';
|
import Loading from 'components/Loading';
|
||||||
|
|
||||||
import { useAppSelector } from 'app/hooks';
|
import { useAppSelector } from 'app/hooks';
|
||||||
|
@ -41,13 +40,13 @@ export const searchPokemonCardsByName = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const Pokedex = () => {
|
const Pokedex = () => {
|
||||||
|
const selectedType = useAppSelector(state => state.filter.selectedType);
|
||||||
|
const selectedSort = useAppSelector(state => state.filter.selectedSort);
|
||||||
|
const searchInput = useAppSelector(state => state.filter.searchInput);
|
||||||
|
|
||||||
const isLoadingPokemons = useAppSelector(
|
const isLoadingPokemons = useAppSelector(
|
||||||
state => state.pokedex.isLoadingPokemons,
|
state => state.pokedex.isLoadingPokemons,
|
||||||
);
|
);
|
||||||
const selectedType = useAppSelector(state => state.pokedex.selectedType);
|
|
||||||
const selectedSort = useAppSelector(state => state.pokedex.selectedSort);
|
|
||||||
const searchInput = useAppSelector(state => state.pokedex.searchInput);
|
|
||||||
|
|
||||||
const pokemonList = useAppSelector(state => state.pokedex.pokemonCardList);
|
const pokemonList = useAppSelector(state => state.pokedex.pokemonCardList);
|
||||||
|
|
||||||
const filteredPokemonList = filterPokemonCardsByType(
|
const filteredPokemonList = filterPokemonCardsByType(
|
||||||
|
@ -65,7 +64,6 @@ const Pokedex = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Filters />
|
|
||||||
{isLoadingPokemons ? (
|
{isLoadingPokemons ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
|
||||||
import {
|
|
||||||
pokeApiAllPagesCustomBaseQuery,
|
|
||||||
pokeApiFullListFetchArgs,
|
|
||||||
} from './paginationBaseQuery';
|
|
||||||
import {
|
|
||||||
AreaResponseData,
|
|
||||||
LocationResponseData,
|
|
||||||
PokemonListResponseData,
|
|
||||||
PokemonResponseData,
|
|
||||||
RegionListResponseData,
|
|
||||||
RegionResponseData,
|
|
||||||
TypeListResponseData,
|
|
||||||
TypeResponseData,
|
|
||||||
PokemonListItem,
|
|
||||||
nameUrlPair,
|
|
||||||
PokemonList,
|
|
||||||
} from './types/api';
|
|
||||||
|
|
||||||
const pokeApiBaseQuery = async (
|
|
||||||
args: pokeApiFullListFetchArgs,
|
|
||||||
api: any,
|
|
||||||
extra: any,
|
|
||||||
) => {
|
|
||||||
const baseUrl = 'https://pokeapi.co/api/v2/';
|
|
||||||
|
|
||||||
if (args.fetchAllPages) {
|
|
||||||
return pokeApiAllPagesCustomBaseQuery(args, api, extra, baseUrl);
|
|
||||||
} else {
|
|
||||||
return fetchBaseQuery({ baseUrl })(args, api, extra);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pokedexApi = createApi({
|
|
||||||
reducerPath: 'pokedexApi',
|
|
||||||
baseQuery: pokeApiBaseQuery,
|
|
||||||
endpoints: builder => ({
|
|
||||||
getPokemonList: builder.query<PokemonListResponseData, void>({
|
|
||||||
query: () => ({ url: `pokemon`, fetchAllPages: true }),
|
|
||||||
}),
|
|
||||||
getRegionList: builder.query<RegionListResponseData, void>({
|
|
||||||
query: () => ({ url: 'region', fetchAllPages: true }),
|
|
||||||
}),
|
|
||||||
getTypeList: builder.query<TypeListResponseData, void>({
|
|
||||||
query: () => ({ url: 'type', fetchAllPages: true }),
|
|
||||||
transformResponse: (response: RegionListResponseData) => {
|
|
||||||
return {
|
|
||||||
...response,
|
|
||||||
results: [{ name: 'All Types', url: '' }, ...response.results],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
getPokemon: builder.query<PokemonResponseData, number | string>({
|
|
||||||
query: IdOrName => ({ url: `pokemon/${IdOrName}` }),
|
|
||||||
}),
|
|
||||||
getRegion: builder.query<RegionResponseData, number | string>({
|
|
||||||
query: IdOrName => ({ url: `region/${IdOrName}` }),
|
|
||||||
}),
|
|
||||||
getType: builder.query<TypeResponseData, number | string>({
|
|
||||||
query: IdOrName => ({ url: `type/${IdOrName}` }),
|
|
||||||
}),
|
|
||||||
getLocation: builder.query<LocationResponseData, number | string>({
|
|
||||||
query: IdOrName => ({ url: `location/${IdOrName}` }),
|
|
||||||
}),
|
|
||||||
getArea: builder.query<AreaResponseData, number | string>({
|
|
||||||
query: IdOrName => ({ url: `location-area/${IdOrName}` }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
useGetPokemonListQuery,
|
|
||||||
useGetRegionListQuery,
|
|
||||||
useGetTypeListQuery,
|
|
||||||
useGetPokemonQuery,
|
|
||||||
useGetRegionQuery,
|
|
||||||
useGetTypeQuery,
|
|
||||||
useGetAreaQuery,
|
|
||||||
useGetLocationQuery,
|
|
||||||
} = pokedexApi;
|
|
|
@ -1,15 +1,11 @@
|
||||||
import { createAsyncThunk, createSlice, Dispatch } from '@reduxjs/toolkit';
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||||
import type { Slice, PayloadAction } from '@reduxjs/toolkit';
|
import type { Slice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { PokedexState, RegionPokemonRange } from 'features/Pokedex/types/slice';
|
import { PokedexState } from 'features/Pokedex/types/slice';
|
||||||
|
|
||||||
import { getStartAndEndIdsForRegion } from './utils';
|
import { getStartAndEndIdsForRegion } from './utils';
|
||||||
import { PokemonResponseData } from './types/api';
|
import { PokemonResponseData } from './types/api';
|
||||||
import { pokedexApi } from './pokedexApi';
|
import { RootState } from 'app/store';
|
||||||
import { RootState } from '../../app/store';
|
|
||||||
|
|
||||||
pokedexApi.endpoints.getTypeList.initiate(); // initialize type list fetching
|
|
||||||
// typesData will be used in Filters.tsx
|
|
||||||
|
|
||||||
export const fetchPokemonsInTheRegion = createAsyncThunk<
|
export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||||
PokemonResponseData[],
|
PokemonResponseData[],
|
||||||
|
@ -17,7 +13,7 @@ export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||||
{ state: RootState }
|
{ state: RootState }
|
||||||
>('pokedex/setSelectedRegion', async (region: string, thunkAPI) => {
|
>('pokedex/setSelectedRegion', async (region: string, thunkAPI) => {
|
||||||
const { dispatch, getState } = thunkAPI;
|
const { dispatch, getState } = thunkAPI;
|
||||||
const regionOptions = getState().pokedex.regionOptions;
|
const regionOptions = getState().filter.regionOptions;
|
||||||
|
|
||||||
dispatch(setIsLoadingPokemons(true));
|
dispatch(setIsLoadingPokemons(true));
|
||||||
|
|
||||||
|
@ -44,13 +40,6 @@ export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialState: PokedexState = {
|
const initialState: PokedexState = {
|
||||||
regionOptions: [],
|
|
||||||
typeOptions: [],
|
|
||||||
sortOptions: [],
|
|
||||||
selectedRegion: '',
|
|
||||||
selectedType: '',
|
|
||||||
selectedSort: '',
|
|
||||||
searchInput: '',
|
|
||||||
isLoadingPokemons: true,
|
isLoadingPokemons: true,
|
||||||
pokemonCardList: [],
|
pokemonCardList: [],
|
||||||
};
|
};
|
||||||
|
@ -59,30 +48,6 @@ export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||||
name: 'pokedex',
|
name: 'pokedex',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setSelectedRegion: (state, action: PayloadAction<string>) => {
|
|
||||||
state.selectedRegion = action.payload;
|
|
||||||
},
|
|
||||||
setSelectedType: (state, action: PayloadAction<string>) => {
|
|
||||||
state.selectedType = action.payload;
|
|
||||||
},
|
|
||||||
setSelectedSort: (state, action: PayloadAction<string>) => {
|
|
||||||
state.selectedSort = action.payload;
|
|
||||||
},
|
|
||||||
setRegionOptions: (state, action: PayloadAction<RegionPokemonRange[]>) => {
|
|
||||||
state.regionOptions = action.payload;
|
|
||||||
},
|
|
||||||
setTypeOptions: (state, action: PayloadAction<string[]>) => {
|
|
||||||
state.typeOptions = action.payload;
|
|
||||||
},
|
|
||||||
setSortOptions: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<{ name: string; value: string }[]>,
|
|
||||||
) => {
|
|
||||||
state.sortOptions = action.payload;
|
|
||||||
},
|
|
||||||
setSearchInput: (state, action: PayloadAction<string>) => {
|
|
||||||
state.searchInput = action.payload;
|
|
||||||
},
|
|
||||||
setIsLoadingPokemons: (state, action: PayloadAction<boolean>) => {
|
setIsLoadingPokemons: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isLoadingPokemons = action.payload;
|
state.isLoadingPokemons = action.payload;
|
||||||
},
|
},
|
||||||
|
@ -101,29 +66,9 @@ export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||||
types: pokemon.types.map(type => type.type.name),
|
types: pokemon.types.map(type => type.type.name),
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
builder.addMatcher(
|
|
||||||
pokedexApi.endpoints.getTypeList.matchFulfilled,
|
|
||||||
(state, action) => {
|
|
||||||
if (action.payload && action.payload.results.length > 0) {
|
|
||||||
const regionListResults = action.payload.results;
|
|
||||||
state.typeOptions = regionListResults.map(region => region.name);
|
|
||||||
|
|
||||||
state.selectedType = action.payload.results[0].name;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const { setIsLoadingPokemons } = pokedexSlice.actions;
|
||||||
setSelectedRegion,
|
|
||||||
setSelectedType,
|
|
||||||
setSelectedSort,
|
|
||||||
setRegionOptions,
|
|
||||||
setTypeOptions,
|
|
||||||
setSortOptions,
|
|
||||||
setSearchInput,
|
|
||||||
setIsLoadingPokemons,
|
|
||||||
} = pokedexSlice.actions;
|
|
||||||
|
|
||||||
export default pokedexSlice.reducer;
|
export default pokedexSlice.reducer;
|
||||||
|
|
|
@ -1,20 +1,7 @@
|
||||||
import { PokemonResponseData } from './api';
|
import { PokemonResponseData } from './api';
|
||||||
import { PokemonCardProps } from '../PokemonCard';
|
import { PokemonCardProps } from 'components/PokemonCard';
|
||||||
|
|
||||||
export type PokedexState = {
|
export type PokedexState = {
|
||||||
regionOptions: RegionPokemonRange[];
|
|
||||||
typeOptions: string[];
|
|
||||||
sortOptions: { name: string; value: string }[];
|
|
||||||
selectedRegion: string;
|
|
||||||
selectedType: string;
|
|
||||||
selectedSort: string;
|
|
||||||
searchInput: string;
|
|
||||||
isLoadingPokemons: boolean;
|
isLoadingPokemons: boolean;
|
||||||
pokemonCardList: PokemonCardProps[];
|
pokemonCardList: PokemonCardProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RegionPokemonRange = {
|
|
||||||
region: string;
|
|
||||||
startId: number;
|
|
||||||
endId: number;
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RegionPokemonRange } from './types/slice';
|
import { RegionPokemonRange } from 'features/Filters/types/slice';
|
||||||
|
|
||||||
export const getStartAndEndIdsForRegion = (
|
export const getStartAndEndIdsForRegion = (
|
||||||
region: string,
|
region: string,
|
||||||
|
|
|
@ -1,3 +1,23 @@
|
||||||
|
html {
|
||||||
|
--toggle: hsl(230deg 17% 85%);
|
||||||
|
--bggradient: url('assets/bg.png');
|
||||||
|
--bgcolor: none;
|
||||||
|
--colorPrimary: #000;
|
||||||
|
--filterHeading: #000;
|
||||||
|
--selectBg: #fff;
|
||||||
|
--selectText: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
--toggle: linear-gradient(90deg, hsl(216deg 52% 48%), hsl(51deg 100% 60%));
|
||||||
|
--bggradient: none;
|
||||||
|
--bgcolor: #16171f;
|
||||||
|
--colorPrimary: #fff;
|
||||||
|
--filterHeading: #707384;
|
||||||
|
--selectBg: #16171f;
|
||||||
|
--selectText: #707384;
|
||||||
|
}
|
||||||
|
|
||||||
.app_container {
|
.app_container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -6,6 +26,11 @@
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'barcadebrawl';
|
||||||
|
src: url('assets/fonts/BarcadeBrawl.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
@ -13,7 +38,7 @@ body {
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-image: url('./assets/bg.png');
|
background-image: url('assets/bg.png');
|
||||||
background-size: initial;
|
background-size: initial;
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
}
|
}
|
||||||
|
@ -23,21 +48,6 @@ code {
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter__container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 7vh 0 5vh 0;
|
|
||||||
gap: 0 2vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter__items {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-family: 'barcadebrawl';
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pokemon__container {
|
.pokemon__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -55,3 +55,17 @@ export const pokeApiAllPagesCustomBaseQuery = async (
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const pokeApiBaseQuery = async (
|
||||||
|
args: pokeApiFullListFetchArgs,
|
||||||
|
api: any,
|
||||||
|
extra: any,
|
||||||
|
) => {
|
||||||
|
const baseUrl = 'https://pokeapi.co/api/v2/';
|
||||||
|
|
||||||
|
if (args.fetchAllPages) {
|
||||||
|
return pokeApiAllPagesCustomBaseQuery(args, api, extra, baseUrl);
|
||||||
|
} else {
|
||||||
|
return fetchBaseQuery({ baseUrl })(args, api, extra);
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue