diff --git a/src/api/pokemon.api.ts b/src/api/pokemon.api.ts new file mode 100644 index 0000000..92cce25 --- /dev/null +++ b/src/api/pokemon.api.ts @@ -0,0 +1,36 @@ +import { Pokemon, PokemonDetailed } from "types/pokemon" + +export const fetchPokemon = async (url: string): Promise<{nextUrl: string, pokemon: Pokemon[]}> => { + try { + const response = await fetch(url) + const { next: nextUrl, results: pokemon }: { next: string, results: Pokemon[] } = await response.json() + return { nextUrl, pokemon } + } catch (error) { + console.error('Error fetching pokemon', error) + return { nextUrl: url, pokemon: [] } + } +} + +export const fetchPokemonDetails = async (detailsUrl: string): Promise => { + try { + const response = await fetch(detailsUrl) + const { id, name, url, sprites: {front_default: front, back_default: back}, types }: { + id: number, + name: string, + url: string, + sprites: { + front_default: string, + back_default: string + }, + types: { + type: { + name: string + } + }[] + } = await response.json() + return { id, name, url, pictureURLs: { front, back }, types: types?.map(({type: {name}}) => name)} + } catch (error) { + console.error('Error fetching pokemon', error) + return null + } +} \ No newline at end of file diff --git a/src/components/pokemonCard.component.tsx b/src/components/pokemonCard.component.tsx new file mode 100644 index 0000000..06af8d1 --- /dev/null +++ b/src/components/pokemonCard.component.tsx @@ -0,0 +1,50 @@ +import { useNavigation } from '@react-navigation/native'; +import { getColorByPokemonType } from '@utils'; +import { usePokemon } from 'contexts/pokemonContext'; +import React from 'react'; +import { Text, StyleSheet, Image, TouchableOpacity } from 'react-native'; +import { PokemonDetailed } from "types/pokemon"; + +export const PokemonCard = ({pokemon}: {pokemon: PokemonDetailed}) => { + const navigation = useNavigation() + const { selectPokemon } = usePokemon() + + const typeColor: string = getColorByPokemonType(pokemon.types?.[0] || '') + + const navigateToPokemonDetails = () => { + selectPokemon(pokemon.id) + navigation.navigate('Pokemon') + } + + return ( + + + {pokemon.name} + + ) +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + height: 75, + width: '45%', + borderRadius: 15, + padding: 10, + alignItems: 'center', + gap: 5 + }, + pokemonImage: { + width: '40%', + height: '100%' + }, + pokemonName: { + textTransform: 'capitalize', + color: 'white', + fontWeight: 'bold', + maxWidth: '50%', + flexWrap: 'nowrap' + } +}); \ No newline at end of file diff --git a/src/contexts/pokemonContext.tsx b/src/contexts/pokemonContext.tsx new file mode 100644 index 0000000..3e478c2 --- /dev/null +++ b/src/contexts/pokemonContext.tsx @@ -0,0 +1,32 @@ +import { createContext, ReactElement, useContext, useState } from "react"; +import { PokemonDetailed } from "types/pokemon"; + +type PokemonContextProps = { + pokemons: PokemonDetailed[] + selectedPokemon?: PokemonDetailed + selectPokemon: (id: number) => void + updatePokemons: (pokemons: PokemonDetailed[]) => void +} + +export const PokemonContext = createContext({ + pokemons: [], + selectedPokemon: undefined, + selectPokemon: () => {}, + updatePokemons: () => {} +}) + +export const usePokemon = () => useContext(PokemonContext) + +export const PokemonContextProvider = ({ children }: {children: ReactElement}) => { + const [pokemons, setPokemons] = useState([]) + const [selectedPokemon, setSelectedPokemon] = useState(undefined) + + const updatePokemons = setPokemons + + const selectPokemon = (id: number) => { + const selectedPokemon: PokemonDetailed | undefined = pokemons.find(p => p.id === id) + setSelectedPokemon(selectedPokemon) + } + + return {children} +} \ No newline at end of file diff --git a/src/navigation/AppStack.tsx b/src/navigation/AppStack.tsx index 62037ac..9f9bc78 100644 --- a/src/navigation/AppStack.tsx +++ b/src/navigation/AppStack.tsx @@ -1,20 +1,34 @@ import React from 'react'; import {createStaticNavigation} from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack'; - -import {PokemonsScreen} from '@screens/pokemons'; +import {PokemonsScreen, PokemonDetailsScreen} from '@screens/index'; +import { PokemonContextProvider } from 'contexts/pokemonContext'; const RootStack = createStackNavigator({ initialRouteName: 'Pokedex', + screenOptions: { + headerTransparent: true, + headerTitleStyle: { + textTransform: 'capitalize', + width: '100%' + }, + }, screens: { Pokedex: PokemonsScreen, + Pokemon: { + screen: PokemonDetailsScreen, + options: { + headerTintColor: 'white', + headerTitle: undefined + }, + }, }, }); const Navigation = createStaticNavigation(RootStack); const AppStack = () => { - return ; + return ; }; export {AppStack}; diff --git a/src/screens/index.ts b/src/screens/index.ts new file mode 100644 index 0000000..c11e0bb --- /dev/null +++ b/src/screens/index.ts @@ -0,0 +1,2 @@ +export * from './pokemon' +export * from './pokemons' \ No newline at end of file diff --git a/src/screens/pokemon/index.ts b/src/screens/pokemon/index.ts new file mode 100644 index 0000000..4a2fecc --- /dev/null +++ b/src/screens/pokemon/index.ts @@ -0,0 +1 @@ +export {PokemonDetailsScreen} from './pokemonDetails.screen.tsx'; diff --git a/src/screens/pokemon/pokemonDetails.screen.tsx b/src/screens/pokemon/pokemonDetails.screen.tsx new file mode 100644 index 0000000..c4adf0f --- /dev/null +++ b/src/screens/pokemon/pokemonDetails.screen.tsx @@ -0,0 +1,66 @@ +import { useNavigation } from "@react-navigation/native" +import { getColorByPokemonType } from "@utils" +import { usePokemon } from "contexts/pokemonContext" +import { useEffect } from "react" +import { Image, StyleSheet, Text, View } from "react-native" +import { SafeAreaView } from "react-native-safe-area-context" + +export const PokemonDetailsScreen = () => { + const navigation = useNavigation() + const { selectedPokemon } = usePokemon() + + const typeColor: string = getColorByPokemonType(selectedPokemon?.types[0] || '') + + useEffect(() => { + navigation.setOptions({ title: selectedPokemon?.name, headerStyle: { backgroundColor: typeColor } }) + }, []) + + const renderType = (type: string, index: number) => ( + + {type} + + ) + + return ( + + + {selectedPokemon?.name.toUpperCase()} + {selectedPokemon?.types.map(renderType)} + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + display: 'flex', + gap: 10, + marginTop: 50 + }, + pokemonImage: { + width: '100%', + height: '50%' + }, + pokemonName: { + fontWeight: 'bold', + fontSize: 30, + textAlign: 'center' + }, + pokemonTypes: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignContent: 'center', + gap: 10 + }, + pokemonType: { + paddingVertical: 5, + paddingHorizontal: 10, + borderRadius: 6 + }, + pokemonTypeText: { + fontWeight: 'bold', + fontSize: 15, + color: 'white' + } +}); \ No newline at end of file diff --git a/src/screens/pokemons/pokemons.screen.tsx b/src/screens/pokemons/pokemons.screen.tsx index ccbd071..50de109 100644 --- a/src/screens/pokemons/pokemons.screen.tsx +++ b/src/screens/pokemons/pokemons.screen.tsx @@ -1,20 +1,66 @@ -import React from 'react'; -import {StyleSheet, Text, View} from 'react-native'; +import { PokemonCard } from '@components/pokemonCard.component'; +import { fetchPokemon, fetchPokemonDetails } from 'api/pokemon.api'; +import { usePokemon } from 'contexts/pokemonContext'; +import React, { useEffect, useState } from 'react'; +import {StyleSheet, FlatList, Image} from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import { Pokemon, PokemonDetailed } from 'types/pokemon'; const PokemonsScreen = () => { + const {pokemons, updatePokemons} = usePokemon() + const [fetchUrl, setFetchUrl] = useState('https://pokeapi.co/api/v2/pokemon') + + const getPokemon = async () => { + const { nextUrl, pokemon: fetchedPokemon }: { nextUrl: string, pokemon: Pokemon[] } = await fetchPokemon(fetchUrl) + setFetchUrl(nextUrl) + const pormiseSettledResult: PromiseSettledResult[] = await Promise.allSettled(fetchedPokemon.map(({url}) => fetchPokemonDetails(url))) + const pokemonsDetails = pormiseSettledResult.filter(promise => promise.status === 'fulfilled' && 'value' in promise && promise.value).map(({value}) => value) + if (pokemonsDetails.some(p => p?.id === 1)) return updatePokemons([...pokemonsDetails]) + const mergedPokemons: PokemonDetailed[] = fetchedPokemon.map(p => { + const pokemonFound: PokemonDetailed = pokemonsDetails.find(pokemonDetail => pokemonDetail?.name === p.name) as PokemonDetailed + if (!pokemonFound) return null + return {...p, ...pokemonFound} as PokemonDetailed + }).filter(p => p) as PokemonDetailed[] + updatePokemons([...pokemons, ...mergedPokemons.filter(p => p)]) + } + + useEffect(() => { + getPokemon() + }, []) + return ( - - Pokemon list screen - + + + + } + onEndReached={getPokemon} + keyExtractor={item => item.id.toString()}/> + + ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', + pokeballImage: { + position: 'absolute', + right: -50, + top: -50, + width: 200, + height: 200, + opacity: 0.1 + }, + listContainer: { + paddingTop: 70 }, -}); + listColumnWrapper: { + paddingVertical: 10, + justifyContent: 'space-evenly' + } +}) export {PokemonsScreen}; diff --git a/src/types/pokemon.ts b/src/types/pokemon.ts new file mode 100644 index 0000000..1b06b9f --- /dev/null +++ b/src/types/pokemon.ts @@ -0,0 +1,13 @@ +export type Pokemon = { + name: string + url: string +} + +export type PokemonDetailed = Pokemon & { + id: number + pictureURLs: { + front: string + back: string + } + types: string[] +} \ No newline at end of file