├── 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 |
19 | );
20 |
--------------------------------------------------------------------------------