Compare commits
9 Commits
91ead7f64f
...
3281629dcf
Author | SHA1 | Date | |
---|---|---|---|
3281629dcf | |||
10442f9dc8 | |||
2857b1d131 | |||
3926267d77 | |||
c10c2f2608 | |||
00a8e7cba1 | |||
609b5621f1 | |||
9338a70918 | |||
2d4426d84d |
@ -1,6 +1,6 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
A simple Pokemon catalogue app, build with React, Redux-Toolkit, Material-UI and PokeAPI.
|
||||
A simple PokemonCard catalogue app, build with React, Redux-Toolkit, Material-UI and PokeAPI.
|
||||
|
||||
It's a learning project following [pokedex](https://github.com/s1varam/pokedex). In this project, I practised following skills:
|
||||
* Use React to create View for FE
|
||||
|
@ -9,6 +9,7 @@
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-lazy-load-image-component": "^1.5.6",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
@ -76,6 +77,7 @@
|
||||
"@types/node": "^16.18.14",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-lazy-load-image-component": "^1.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"@typescript-eslint/parser": "^5.56.0",
|
||||
"babel-plugin-named-exports-order": "^0.0.2",
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
setRegionOptions,
|
||||
setSortOptions,
|
||||
setTypeOptions,
|
||||
setSearchInput,
|
||||
} from 'features/Pokedex/pokedexSlice';
|
||||
import { RegionPokemonRange } from 'features/Pokedex/types/slice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/hooks';
|
||||
@ -141,6 +142,15 @@ const Filters = () => {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter__items">
|
||||
<div>
|
||||
<div>SEARCH</div>
|
||||
<input
|
||||
type="text"
|
||||
onChange={e => dispatch(setSearchInput(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,24 +1,23 @@
|
||||
import React from 'react';
|
||||
import Pokemon from './Pokemon';
|
||||
import PokemonCard, { PokemonCardProps } from './PokemonCard';
|
||||
import Filters from './Filters';
|
||||
import Loading from 'components/Loading';
|
||||
|
||||
import { useAppSelector } from 'app/hooks';
|
||||
import { PokemonResponseData } from './types/api';
|
||||
|
||||
export const filterPokemonByType = (
|
||||
pokemonList: PokemonResponseData[],
|
||||
export const filterPokemonCardsByType = (
|
||||
pokemonList: PokemonCardProps[],
|
||||
selectedType: string,
|
||||
) => {
|
||||
return pokemonList.filter(
|
||||
pokemon =>
|
||||
selectedType === 'All Types' ||
|
||||
pokemon.types.some(type => type.type.name === selectedType),
|
||||
pokemon.types.some(type => type === selectedType),
|
||||
);
|
||||
};
|
||||
|
||||
export const sortPokemonsByIdOrName = (
|
||||
pokemonList: PokemonResponseData[],
|
||||
export const sortPokemonCardsByIdOrName = (
|
||||
pokemonList: PokemonCardProps[],
|
||||
selectedSort: string,
|
||||
) => {
|
||||
return pokemonList.sort((a, b) => {
|
||||
@ -32,20 +31,37 @@ export const sortPokemonsByIdOrName = (
|
||||
});
|
||||
};
|
||||
|
||||
export const searchPokemonCardsByName = (
|
||||
pokemonList: PokemonCardProps[],
|
||||
searchInput: string,
|
||||
) => {
|
||||
return pokemonList.filter(pokemon =>
|
||||
pokemon.name.toLowerCase().includes(searchInput.toLowerCase()),
|
||||
);
|
||||
};
|
||||
|
||||
const Pokedex = () => {
|
||||
const isLoadingPokemons = useAppSelector(
|
||||
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.pokemonList);
|
||||
const pokemonList = useAppSelector(state => state.pokedex.pokemonCardList);
|
||||
|
||||
const filteredPokemonList = filterPokemonByType(pokemonList, selectedType);
|
||||
const sortedFilteredPokemonList = sortPokemonsByIdOrName(
|
||||
const filteredPokemonList = filterPokemonCardsByType(
|
||||
pokemonList,
|
||||
selectedType,
|
||||
);
|
||||
const sortedFilteredPokemonCardList = sortPokemonCardsByIdOrName(
|
||||
filteredPokemonList,
|
||||
selectedSort,
|
||||
);
|
||||
const searchedPokemonCardList = searchPokemonCardsByName(
|
||||
sortedFilteredPokemonCardList,
|
||||
searchInput,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -53,15 +69,17 @@ const Pokedex = () => {
|
||||
{isLoadingPokemons ? (
|
||||
<Loading />
|
||||
) : (
|
||||
sortedFilteredPokemonList.map(pokemon => (
|
||||
<Pokemon
|
||||
key={pokemon.id}
|
||||
name={pokemon.name}
|
||||
number={pokemon.id}
|
||||
image={pokemon.sprites.other.dream_world.front_default}
|
||||
types={pokemon.types.map(type => type.type.name)}
|
||||
/>
|
||||
))
|
||||
<div className="all__pokemons">
|
||||
{searchedPokemonCardList.map(pokemonCard => (
|
||||
<PokemonCard
|
||||
key={pokemonCard.id}
|
||||
id={pokemonCard.id}
|
||||
name={pokemonCard.name}
|
||||
image={pokemonCard.image}
|
||||
types={pokemonCard.types}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -1,2 +0,0 @@
|
||||
export * from './Pokemon';
|
||||
export { default } from './Pokemon';
|
@ -2,33 +2,33 @@ import React from 'react';
|
||||
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Pokemon, { PokemonCardProps } from './Pokemon';
|
||||
import PokemonCard, { PokemonCardProps } from './PokemonCard';
|
||||
|
||||
import charizard_svg from './assets/stories/charizard.svg';
|
||||
import charizard_info from './assets/stories/charizard.json';
|
||||
|
||||
export default {
|
||||
title: 'Pokedex/PokemonCard',
|
||||
component: Pokemon,
|
||||
} as ComponentMeta<typeof Pokemon>;
|
||||
component: PokemonCard,
|
||||
} as ComponentMeta<typeof PokemonCard>;
|
||||
|
||||
const Template: ComponentStory<typeof Pokemon> = (args: PokemonCardProps) => (
|
||||
<Pokemon {...args} />
|
||||
);
|
||||
const Template: ComponentStory<typeof PokemonCard> = (
|
||||
args: PokemonCardProps,
|
||||
) => <PokemonCard {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
|
||||
Primary.args = {
|
||||
id: 6,
|
||||
name: charizard_info.name,
|
||||
number: 6,
|
||||
image: charizard_svg,
|
||||
types: ['fire', 'flying'],
|
||||
};
|
||||
|
||||
export const Charizard = Template.bind({});
|
||||
Charizard.args = {
|
||||
id: 6,
|
||||
name: charizard_info.name,
|
||||
number: 6,
|
||||
image: charizard_svg,
|
||||
types: ['fire', 'flying'],
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { formatNumber } from './Pokemon';
|
||||
import { formatNumber } from './PokemonCard';
|
||||
|
||||
describe('Test Functions', () => {
|
||||
describe('formatNumber', () => {
|
@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
||||
import 'react-lazy-load-image-component/src/effects/blur.css';
|
||||
import { Tooltip, Zoom } from '@mui/material';
|
||||
|
||||
import './Pokemon.css';
|
||||
import './PokemonCard.css';
|
||||
import * as pokeTypeAsset from './assets';
|
||||
import { colorTypeGradients } from './utils';
|
||||
|
||||
export interface PokemonProps {
|
||||
export interface PokemonCardProps {
|
||||
id: number;
|
||||
name: string;
|
||||
number: number;
|
||||
image: string;
|
||||
types: string[];
|
||||
}
|
||||
@ -59,7 +61,12 @@ function findPokeTypeAsset(pokeType: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function Pokemon({ name, number, image, types }: PokemonProps) {
|
||||
export default function PokemonCard({
|
||||
id,
|
||||
name,
|
||||
image,
|
||||
types,
|
||||
}: PokemonCardProps) {
|
||||
let finalColor;
|
||||
|
||||
if (types.length === 2) {
|
||||
@ -76,7 +83,7 @@ export default function Pokemon({ name, number, image, types }: PokemonProps) {
|
||||
}}
|
||||
>
|
||||
<div className="card__header">
|
||||
<div className="poke__number">{formatNumber(number)}</div>
|
||||
<div className="poke__number">{formatNumber(id)}</div>
|
||||
<div className="info__icon">
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
@ -92,7 +99,15 @@ export default function Pokemon({ name, number, image, types }: PokemonProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="image__container">
|
||||
<img src={image} alt={name} />
|
||||
<LazyLoadImage
|
||||
alt={name}
|
||||
height={150}
|
||||
src={image}
|
||||
visibleByDefault={false}
|
||||
delayMethod={'debounce'}
|
||||
effect="blur"
|
||||
className="img_thumbnail"
|
||||
/>
|
||||
</div>
|
||||
<div className="poke__name">
|
||||
<h3>{name}</h3>
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
2
src/features/Pokedex/PokemonCard/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './PokemonCard';
|
||||
export { default } from './PokemonCard';
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
filterPokemonByType,
|
||||
sortPokemonsByIdOrName,
|
||||
filterPokemonCardsByType,
|
||||
sortPokemonCardsByIdOrName,
|
||||
searchPokemonCardsByName,
|
||||
} from 'features/Pokedex/Pokedex';
|
||||
import { PokemonResponseData } from 'features/Pokedex/types/api';
|
||||
import pokemon3_Venusaur from 'features/Pokedex/__test__/pokemon3_Venusaur.json';
|
||||
@ -34,15 +35,15 @@ describe('pokedex Component', () => {
|
||||
pokemon4_Charmander,
|
||||
];
|
||||
|
||||
it('should return all Pokemon if the selected type is "All Types"', () => {
|
||||
it('should return all PokemonCard if the selected type is "All Types"', () => {
|
||||
const selectedType = 'All Types';
|
||||
const filteredList = filterPokemonByType(pokemonList, selectedType);
|
||||
const filteredList = filterPokemonCardsByType(pokemonList, selectedType);
|
||||
expect(filteredList).toEqual(pokemonList);
|
||||
});
|
||||
|
||||
it('should return only Pokemon of the selected type', () => {
|
||||
it('should return only PokemonCard of the selected type', () => {
|
||||
const selectedType = 'fire';
|
||||
const filteredList = filterPokemonByType(pokemonList, selectedType);
|
||||
const filteredList = filterPokemonCardsByType(pokemonList, selectedType);
|
||||
const allPokemonAreOfTypeFire = filteredList.every(pokemon =>
|
||||
pokemon.types.some(type => type.type.name === selectedType),
|
||||
);
|
||||
@ -71,14 +72,42 @@ describe('pokedex Component', () => {
|
||||
];
|
||||
it('should sort by id if the selected sort is "id"', () => {
|
||||
const selectedSort = 'id';
|
||||
const sortedList = sortPokemonsByIdOrName(pokemonList, selectedSort);
|
||||
const sortedList = sortPokemonCardsByIdOrName(pokemonList, selectedSort);
|
||||
expect(sortedList).toEqual([pokemon3_Venusaur, pokemon4_Charmander]);
|
||||
});
|
||||
|
||||
it('should sort by name if the selected sort is "name"', () => {
|
||||
const selectedSort = 'name';
|
||||
const sortedList = sortPokemonsByIdOrName(pokemonList, selectedSort);
|
||||
const sortedList = sortPokemonCardsByIdOrName(pokemonList, selectedSort);
|
||||
expect(sortedList).toEqual([pokemon4_Charmander, pokemon3_Venusaur]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchPokemonByName works correctly', () => {
|
||||
beforeEach(() => {
|
||||
store = configureStore({
|
||||
reducer: {
|
||||
pokedex: pokedexSlice.reducer,
|
||||
[pokedexApi.reducerPath]: pokedexApi.reducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware().concat(
|
||||
pokedexApi.middleware,
|
||||
listenerMiddleware.middleware,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
const pokemonList: PokemonResponseData[] = [
|
||||
pokemon3_Venusaur,
|
||||
pokemon4_Charmander,
|
||||
];
|
||||
|
||||
it('should search by name correctly', () => {
|
||||
const searchName = 'char';
|
||||
const searchedList = searchPokemonCardsByName(pokemonList, searchName);
|
||||
expect(searchedList).toHaveLength(1);
|
||||
expect(searchedList[0]).toEqual(pokemon4_Charmander);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -19,23 +19,27 @@ export const fetchPokemonsInTheRegion = createAsyncThunk<
|
||||
const { dispatch, getState } = thunkAPI;
|
||||
const regionOptions = getState().pokedex.regionOptions;
|
||||
|
||||
dispatch(setIsLoadingPokemons(true));
|
||||
|
||||
const { startId, endId } = getStartAndEndIdsForRegion(region, regionOptions);
|
||||
const pokemonIds = Array.from(
|
||||
{ length: endId - startId + 1 },
|
||||
(_, i) => i + startId,
|
||||
);
|
||||
// use pokemonIds to fetch pokemon data using getPokemonQuery and store in state
|
||||
// use pokemonIds to fetch pokemon data using fetch, which won't save the data in the cache
|
||||
const pokemonList = await Promise.all(
|
||||
pokemonIds.map(
|
||||
id =>
|
||||
dispatch(pokedexApi.endpoints.getPokemon.initiate(id)) as Promise<{
|
||||
data: PokemonResponseData;
|
||||
}>,
|
||||
fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res =>
|
||||
res.json(),
|
||||
) as Promise<PokemonResponseData>,
|
||||
),
|
||||
);
|
||||
|
||||
const pokemonListData = pokemonList.map(
|
||||
(pokemon: { data: PokemonResponseData }) => pokemon.data,
|
||||
(pokemon: PokemonResponseData) => pokemon,
|
||||
);
|
||||
|
||||
return pokemonListData;
|
||||
});
|
||||
|
||||
@ -46,8 +50,9 @@ const initialState: PokedexState = {
|
||||
selectedRegion: '',
|
||||
selectedType: '',
|
||||
selectedSort: '',
|
||||
searchInput: '',
|
||||
isLoadingPokemons: true,
|
||||
pokemonList: [],
|
||||
pokemonCardList: [],
|
||||
};
|
||||
|
||||
export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||
@ -56,8 +61,6 @@ export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||
reducers: {
|
||||
setSelectedRegion: (state, action: PayloadAction<string>) => {
|
||||
state.selectedRegion = action.payload;
|
||||
// call fetchPokemonsInTheRegion
|
||||
fetchPokemonsInTheRegion(action.payload);
|
||||
},
|
||||
setSelectedType: (state, action: PayloadAction<string>) => {
|
||||
state.selectedType = action.payload;
|
||||
@ -77,18 +80,26 @@ export const pokedexSlice: Slice<PokedexState> = createSlice({
|
||||
) => {
|
||||
state.sortOptions = action.payload;
|
||||
},
|
||||
setSearchInput: (state, action: PayloadAction<string>) => {
|
||||
state.searchInput = action.payload;
|
||||
},
|
||||
setIsLoadingPokemons: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoadingPokemons = action.payload;
|
||||
},
|
||||
setPokemonList: (state, action: PayloadAction<PokemonResponseData[]>) => {
|
||||
state.pokemonList = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
// add fetchPokemonsInTheRegion
|
||||
builder.addCase(fetchPokemonsInTheRegion.fulfilled, (state, action) => {
|
||||
state.isLoadingPokemons = false;
|
||||
state.pokemonList = action.payload;
|
||||
// set action payload to pokemonCardList by transforming payload
|
||||
state.pokemonCardList = action.payload.map(pokemon => ({
|
||||
id: pokemon.id,
|
||||
name: pokemon.name,
|
||||
image: pokemon.sprites.other.dream_world.front_default
|
||||
? pokemon.sprites.other.dream_world.front_default
|
||||
: pokemon.sprites.other['official-artwork'].front_default,
|
||||
types: pokemon.types.map(type => type.type.name),
|
||||
}));
|
||||
});
|
||||
builder.addMatcher(
|
||||
pokedexApi.endpoints.getTypeList.matchFulfilled,
|
||||
@ -111,7 +122,8 @@ export const {
|
||||
setRegionOptions,
|
||||
setTypeOptions,
|
||||
setSortOptions,
|
||||
setPokemonList,
|
||||
setSearchInput,
|
||||
setIsLoadingPokemons,
|
||||
} = pokedexSlice.actions;
|
||||
|
||||
export default pokedexSlice.reducer;
|
||||
|
@ -76,6 +76,9 @@ export interface PokemonResponseData {
|
||||
dream_world: {
|
||||
front_default: string;
|
||||
};
|
||||
'official-artwork': {
|
||||
front_default: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { PokemonResponseData } from './api';
|
||||
import { PokemonCardProps } from '../PokemonCard';
|
||||
|
||||
export type PokedexState = {
|
||||
selectedRegion: string;
|
||||
regionOptions: RegionPokemonRange[];
|
||||
selectedType: string;
|
||||
typeOptions: string[];
|
||||
selectedSort: string;
|
||||
sortOptions: { name: string; value: string }[];
|
||||
selectedRegion: string;
|
||||
selectedType: string;
|
||||
selectedSort: string;
|
||||
searchInput: string;
|
||||
isLoadingPokemons: boolean;
|
||||
pokemonList: PokemonResponseData[];
|
||||
pokemonCardList: PokemonCardProps[];
|
||||
};
|
||||
|
||||
export type RegionPokemonRange = {
|
||||
|
@ -44,3 +44,10 @@ code {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.all__pokemons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
21
yarn.lock
@ -3649,6 +3649,14 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-lazy-load-image-component@^1.5.2":
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-lazy-load-image-component/-/react-lazy-load-image-component-1.5.2.tgz#b87e814b6b91853b802f04465364ff1e913dce6a"
|
||||
integrity sha512-4NLJsMJVrMv18FuMIkUUBVj/PH9A+BvLKrZC75EWiEFn1IsMrZHgL1tVKw5QBfoa0Qjz6SkWIzEvwcyZ8PgnIg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react-transition-group@^4.4.5":
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
|
||||
@ -10230,6 +10238,11 @@ lodash.sortby@^4.7.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
|
||||
|
||||
lodash.throttle@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
|
||||
|
||||
lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
@ -12636,6 +12649,14 @@ react-is@^18.0.0, react-is@^18.2.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
|
||||
react-lazy-load-image-component@^1.5.6:
|
||||
version "1.5.6"
|
||||
resolved "https://registry.yarnpkg.com/react-lazy-load-image-component/-/react-lazy-load-image-component-1.5.6.tgz#a4b84257be21b1825680b4e158d167c08aeda5ff"
|
||||
integrity sha512-M0jeJtOlTHgThOfgYM9krSqYbR6ShxROy/KVankwbw9/amPKG1t5GSGN1sei6Cyu8+QJVuyAUvQ+LFtCVTTlKw==
|
||||
dependencies:
|
||||
lodash.debounce "^4.0.8"
|
||||
lodash.throttle "^4.1.1"
|
||||
|
||||
react-merge-refs@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
|
||||
|