Compare commits

..

No commits in common. "6acabeb91ac767a826831b82a8326b73c0d081d1" and "ec0af2f3acd9876a3b6ed92ac41c6484fe04312c" have entirely different histories.

38 changed files with 231 additions and 920 deletions

View File

@ -1,5 +1,3 @@
import 'index.css';
export const parameters = { export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },
controls: { controls: {

View File

@ -1,14 +1,12 @@
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>
); );

View File

@ -1,21 +1,19 @@
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 slices // api
[filterApi.reducerPath]: filterApi.reducer, [pokedexApi.reducerPath]: pokedexApi.reducer,
}, },
middleware: getDefaultMiddleware => middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat( getDefaultMiddleware().concat(
filterApi.middleware, pokedexApi.middleware,
listenerMiddleware.middleware, listenerMiddleware.middleware,
), ),
devTools: true, devTools: true,

Binary file not shown.

View File

@ -1,7 +0,0 @@
.fa-mars {
color: #7070ff;
}
.fa-venus {
color: rgb(224, 61, 88);
}

View File

@ -1,22 +0,0 @@
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,
};

View File

@ -1,122 +0,0 @@
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;

View File

@ -1 +0,0 @@
export { default } from './GenderRate';

View File

@ -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';
export const findPokeTypeAsset = (pokeType: string) => { function findPokeTypeAsset(pokeType: string) {
switch (pokeType) { switch (pokeType) {
case 'normal': case 'normal':
return pokeTypeAsset.pokeType_normal; return pokeTypeAsset.pokeType_normal;
@ -46,7 +46,7 @@ export const findPokeTypeAsset = (pokeType: string) => {
default: default:
return pokeTypeAsset.pokeType_normal; return pokeTypeAsset.pokeType_normal;
} }
}; }
export interface PokemonTypesProps { export interface PokemonTypesProps {
types: string[]; types: string[];
@ -54,8 +54,7 @@ export interface PokemonTypesProps {
const PokemonTypes = ({ types }: PokemonTypesProps) => { const PokemonTypes = ({ types }: PokemonTypesProps) => {
return ( return (
// css is set in consumer <div className="poke__type">
<>
{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}`}>
@ -63,7 +62,7 @@ const PokemonTypes = ({ types }: PokemonTypesProps) => {
</div> </div>
</Tooltip> </Tooltip>
))} ))}
</> </div>
); );
}; };

View File

@ -1,2 +0,0 @@
export * from './PokemonTypes';
export { default } from './PokemonTypes';

View File

@ -1,42 +0,0 @@
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);
}

View File

@ -1,24 +0,0 @@
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;

View File

@ -1,72 +0,0 @@
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;

View File

@ -1 +0,0 @@
export { default } from './Filters';

View File

@ -1,15 +0,0 @@
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;
};

View File

@ -1,350 +0,0 @@
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;
}
}

View File

@ -1,11 +0,0 @@
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} />
);

View File

@ -1,132 +0,0 @@
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;

View File

@ -1,2 +0,0 @@
export * from './InfoDialog';
export { default } from './InfoDialog';

View File

@ -1,19 +1,17 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from 'app/hooks'; import { useGetTypeListQuery } from 'features/Pokedex/pokedexApi';
import { fetchPokemonsInTheRegion } from 'features/Pokedex/pokedexSlice';
import { import {
setSelectedRegion, setSelectedRegion,
setSelectedType, setSelectedType,
setSelectedSort, setSelectedSort,
fetchPokemonsInTheRegion,
setRegionOptions, setRegionOptions,
setTypeOptions,
setSortOptions, setSortOptions,
setTypeOptions,
setSearchInput, setSearchInput,
} from './filterSlice'; } from 'features/Pokedex/pokedexSlice';
import { useGetTypeListQuery } from './filterApi'; import { RegionPokemonRange } from 'features/Pokedex/types/slice';
import { RegionPokemonRange } from './types/slice'; import { useAppDispatch, useAppSelector } from 'app/hooks';
import './Filters.css';
export const createRegionPokemonListOptionElements = ( export const createRegionPokemonListOptionElements = (
data: RegionPokemonRange[], data: RegionPokemonRange[],
@ -56,12 +54,13 @@ const useGetSortOptions = () => {
const Filters = () => { const Filters = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selectedRegion = useAppSelector(state => state.filter.selectedRegion); const selectedRegion = useAppSelector(state => state.pokedex.selectedRegion);
const selectedType = useAppSelector(state => state.filter.selectedType); const selectedType = useAppSelector(state => state.pokedex.selectedType);
const selectedSort = useAppSelector(state => state.filter.selectedSort); const selectedSort = useAppSelector(state => state.pokedex.selectedSort);
const searchInput = useAppSelector(state => state.filter.searchInput);
const regionPokemonList = useAppSelector(state => state.filter.regionOptions); const regionPokemonList = useAppSelector(
state => state.pokedex.regionOptions,
);
const { data: fetchedRegionOptions } = useGetRegionOptions(); const { data: fetchedRegionOptions } = useGetRegionOptions();
const { data: fetchedTypeOptions, isLoading: isFetchingTypeOptions } = const { data: fetchedTypeOptions, isLoading: isFetchingTypeOptions } =
@ -90,7 +89,7 @@ const Filters = () => {
return ( return (
<> <>
<div className="filter__container noselect"> <div className="filter__container">
<div className="filter__items"> <div className="filter__items">
<div> <div>
<div>REGION</div> <div>REGION</div>
@ -149,7 +148,6 @@ 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>

View File

@ -0,0 +1,2 @@
export { default } from './Filters';
export { default as useFilterLoaded } from './useFilterLoaded';

View File

@ -0,0 +1,18 @@
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;

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PokemonCard, { PokemonCardProps } from 'components/PokemonCard'; import PokemonCard, { PokemonCardProps } from './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';
@ -40,13 +41,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(
@ -64,6 +65,7 @@ const Pokedex = () => {
return ( return (
<> <>
<Filters />
{isLoadingPokemons ? ( {isLoadingPokemons ? (
<Loading /> <Loading />
) : ( ) : (

View File

@ -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;
width: 220px; min-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,30 +96,6 @@ 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);

View File

@ -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: 'Component/PokemonCard', title: 'Pokedex/PokemonCard',
component: PokemonCard, component: PokemonCard,
} as ComponentMeta<typeof PokemonCard>; } as ComponentMeta<typeof PokemonCard>;

View File

@ -17,7 +17,12 @@ export function formatNumber(num: number) {
return '#' + num.toString().padStart(3, '0'); return '#' + num.toString().padStart(3, '0');
} }
const PokemonCard = ({ id, name, image, types }: PokemonCardProps) => { export default function PokemonCard({
id,
name,
image,
types,
}: PokemonCardProps) {
let finalColor; let finalColor;
if (types.length === 2) { if (types.length === 2) {
@ -62,12 +67,8 @@ const PokemonCard = ({ id, name, image, types }: PokemonCardProps) => {
</div> </div>
<div className="poke__name"> <div className="poke__name">
<h3>{name}</h3> <h3>{name}</h3>
<div className="poke__type"> <PokemonTypes types={types} />
<PokemonTypes types={types} />
</div>
</div> </div>
</div> </div>
); );
}; }
export default PokemonCard;

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -55,17 +55,3 @@ 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);
}
};

View File

@ -0,0 +1,80 @@
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;

View File

@ -1,11 +1,15 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice, Dispatch } from '@reduxjs/toolkit';
import type { Slice, PayloadAction } from '@reduxjs/toolkit'; import type { Slice, PayloadAction } from '@reduxjs/toolkit';
import { PokedexState } from 'features/Pokedex/types/slice'; import { PokedexState, RegionPokemonRange } from 'features/Pokedex/types/slice';
import { getStartAndEndIdsForRegion } from './utils'; import { getStartAndEndIdsForRegion } from './utils';
import { PokemonResponseData } from './types/api'; import { PokemonResponseData } from './types/api';
import { RootState } from 'app/store'; import { pokedexApi } from './pokedexApi';
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[],
@ -13,7 +17,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().filter.regionOptions; const regionOptions = getState().pokedex.regionOptions;
dispatch(setIsLoadingPokemons(true)); dispatch(setIsLoadingPokemons(true));
@ -40,6 +44,13 @@ export const fetchPokemonsInTheRegion = createAsyncThunk<
}); });
const initialState: PokedexState = { const initialState: PokedexState = {
regionOptions: [],
typeOptions: [],
sortOptions: [],
selectedRegion: '',
selectedType: '',
selectedSort: '',
searchInput: '',
isLoadingPokemons: true, isLoadingPokemons: true,
pokemonCardList: [], pokemonCardList: [],
}; };
@ -48,6 +59,30 @@ 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;
}, },
@ -66,9 +101,29 @@ 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 { setIsLoadingPokemons } = pokedexSlice.actions; export const {
setSelectedRegion,
setSelectedType,
setSelectedSort,
setRegionOptions,
setTypeOptions,
setSortOptions,
setSearchInput,
setIsLoadingPokemons,
} = pokedexSlice.actions;
export default pokedexSlice.reducer; export default pokedexSlice.reducer;

View File

@ -1,7 +1,20 @@
import { PokemonResponseData } from './api'; import { PokemonResponseData } from './api';
import { PokemonCardProps } from 'components/PokemonCard'; import { PokemonCardProps } from '../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;
};

View File

@ -1,4 +1,4 @@
import { RegionPokemonRange } from 'features/Filters/types/slice'; import { RegionPokemonRange } from './types/slice';
export const getStartAndEndIdsForRegion = ( export const getStartAndEndIdsForRegion = (
region: string, region: string,

View File

@ -1,23 +1,3 @@
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;
@ -26,11 +6,6 @@ html[data-theme='dark'] {
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',
@ -38,7 +13,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;
} }
@ -48,6 +23,21 @@ 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;