├── assets ├── icon.png ├── favicon.png ├── splash.png └── adaptive-icon.png ├── babel.config.js ├── .eslintrc.js ├── .gitignore ├── app.json ├── App.js ├── package.json ├── components ├── Main.jsx ├── GameCard.jsx └── Logo.jsx └── lib └── metacritic.js /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-native-expo-curso-2024/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-native-expo-curso-2024/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-native-expo-curso-2024/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-native-expo-curso-2024/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | module.exports = { 3 | extends: ["expo", "prettier"], 4 | plugins: ["prettier"], 5 | rules: { 6 | "prettier/prettier": "error", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "curso-react-native-metacritic-app", 4 | "slug": "curso-react-native-metacritic-app", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "ios": { 15 | "supportsTablet": true 16 | }, 17 | "android": { 18 | "adaptiveIcon": { 19 | "foregroundImage": "./assets/adaptive-icon.png", 20 | "backgroundColor": "#ffffff" 21 | } 22 | }, 23 | "web": { 24 | "favicon": "./assets/favicon.png" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { StyleSheet, View } from "react-native"; 3 | import { SafeAreaProvider } from "react-native-safe-area-context"; 4 | 5 | import { Main } from "./components/Main"; 6 | import { Logo } from "./components/Logo"; 7 | 8 | export default function App() { 9 | return ( 10 | 11 | 12 | 13 |
14 | 15 | 16 | ); 17 | } 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | flex: 1, 22 | backgroundColor: "#000", 23 | alignItems: "center", 24 | justifyContent: "center", 25 | paddingHorizontal: 12, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "curso-react-native-metacritic-app", 3 | "version": "1.0.0", 4 | "main": "expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "lint": "eslint ." 11 | }, 12 | "dependencies": { 13 | "@expo/metro-runtime": "~3.2.1", 14 | "expo": "~51.0.18", 15 | "expo-status-bar": "~1.12.1", 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "react-native": "0.74.3", 19 | "react-native-web": "~0.19.10", 20 | "expo-constants": "~16.0.2", 21 | "react-native-safe-area-context": "4.10.1", 22 | "react-native-svg": "15.2.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.20.0", 26 | "eslint": "^8.57.0", 27 | "eslint-config-expo": "^7.0.0", 28 | "eslint-config-prettier": "^9.1.0", 29 | "eslint-plugin-prettier": "^5.1.3", 30 | "prettier": "^3.3.2" 31 | }, 32 | "private": true 33 | } 34 | -------------------------------------------------------------------------------- /components/Main.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { FlatList, View, ScrollView, ActivityIndicator } from "react-native"; 4 | import { getLatestGames } from "../lib/metacritic"; 5 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 6 | import { AnimatedGameCard } from "./GameCard"; 7 | import { Logo } from "./Logo"; 8 | 9 | export function Main() { 10 | const [games, setGames] = useState([]); 11 | const insets = useSafeAreaInsets(); 12 | 13 | useEffect(() => { 14 | getLatestGames().then((games) => { 15 | setGames(games); 16 | }); 17 | }, []); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | {games.length === 0 ? ( 25 | 26 | ) : ( 27 | game.slug} 30 | renderItem={({ item, index }) => ( 31 | 32 | )} 33 | /> 34 | )} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/GameCard.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { View, StyleSheet, Text, Image, Animated } from "react-native"; 3 | 4 | export function GameCard({ game }) { 5 | return ( 6 | 7 | 8 | {game.title} 9 | {game.score} 10 | {game.description} 11 | 12 | ); 13 | } 14 | 15 | export function AnimatedGameCard({ game, index }) { 16 | const opacity = useRef(new Animated.Value(0)).current; 17 | 18 | useEffect(() => { 19 | Animated.timing(opacity, { 20 | toValue: 1, 21 | duration: 1000, 22 | delay: index * 250, 23 | useNativeDriver: true, 24 | }).start(); 25 | }, [opacity, index]); 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | card: { 36 | marginBottom: 42, 37 | }, 38 | image: { 39 | width: 107, 40 | height: 147, 41 | borderRadius: 10, 42 | }, 43 | title: { 44 | fontSize: 20, 45 | fontWeight: "bold", 46 | color: "#fff", 47 | marginTop: 10, 48 | }, 49 | description: { 50 | fontSize: 16, 51 | color: "#eee", 52 | }, 53 | score: { 54 | fontSize: 20, 55 | fontWeight: "bold", 56 | color: "green", 57 | marginBottom: 10, 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /lib/metacritic.js: -------------------------------------------------------------------------------- 1 | export async function getLatestGames() { 2 | const LATEST_GAMES = 3 | "https://internal-prod.apigee.fandom.net/v1/xapi/finder/metacritic/web?sortBy=-metaScore&productType=games&page=1&releaseYearMin=1958&releaseYearMax=2024&offset=0&limit=24&apiKey=1MOZgmNFxvmljaQR1X9KAij9Mo4xAY3u"; 4 | 5 | const rawData = await fetch(LATEST_GAMES); 6 | const json = await rawData.json(); 7 | 8 | const { 9 | data: { items }, 10 | } = json; 11 | 12 | return items.map((item) => { 13 | const { description, slug, releaseDate, image, criticScoreSummary, title } = 14 | item; 15 | const { score } = criticScoreSummary; 16 | 17 | // crea la imagen 18 | const { bucketType, bucketPath } = image; 19 | const img = `https://www.metacritic.com/a/img/${bucketType}${bucketPath}`; 20 | 21 | return { 22 | description, 23 | releaseDate, 24 | score, 25 | slug, 26 | title, 27 | image: img, 28 | }; 29 | }); 30 | } 31 | 32 | export async function getGameDetails(slug) { 33 | const GAME_DETAILS = `https://internal-prod.apigee.fandom.net/v1/xapi/composer/metacritic/pages/games/${slug}/web?&apiKey=1MOZgmNFxvmljaQR1X9KAij9Mo4xAY3u`; 34 | 35 | const rawData = await fetch(GAME_DETAILS); 36 | const json = await rawData.json(); 37 | 38 | const { components } = json; 39 | const { title, description, criticScoreSummary, images } = components[0]; 40 | const { score } = criticScoreSummary; 41 | 42 | // get the card image 43 | const cardImage = images.find((image) => image.typeName === "cardImage"); 44 | const { bucketType, bucketPath } = cardImage; 45 | const img = `https://www.metacritic.com/a/img/${bucketType}${bucketPath}`; 46 | 47 | const rawReviews = components[3].data.items; 48 | 49 | // get the reviews 50 | const reviews = rawReviews.map((review) => { 51 | const { quote, score, date, publicationName, author } = review; 52 | return { quote, score, date, publicationName, author }; 53 | }); 54 | 55 | return { 56 | img, 57 | title, 58 | slug, 59 | description, 60 | score, 61 | reviews, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path } from "react-native-svg"; 2 | 3 | export const Logo = (props) => ( 4 | 5 | 6 | 10 | 14 | 18 | 19 | ); 20 | --------------------------------------------------------------------------------