├── .expo-shared └── assets.json ├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── demo.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── index.d.ts ├── package.json ├── src ├── assets │ └── images │ │ └── card-back.png ├── components │ ├── buttons │ │ ├── Button.tsx │ │ └── index.ts │ └── cards │ │ ├── GameCard.tsx │ │ ├── Stats.tsx │ │ └── index.tsx ├── constants │ ├── Theme.ts │ ├── emojis.ts │ └── index.ts ├── hooks │ └── useMatchGame.ts ├── index.tsx ├── screens │ └── MatchThePairs.tsx └── utils │ └── shareGame.ts ├── tsconfig.json └── yarn.lock /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import App from "src/index"; 2 | export default App; 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Expo/React Native Match the Pairs Game 2 | 3 | [Design based on Netlify's Deploy Game](https://www.netlify.com/). 4 | 5 | A quick game that let's you match 8 pairs of emoji's and share your results. 6 | 7 | Features include: 8 | 9 | - Randomized list of emojis so you have new emojis each game 10 | - Flip animation with Reanimated 11 | - Share capability with the React Native Share API 12 | 13 | ![Screenshots of app on iOS, Web, and Android](./assets/demo.png) 14 | 15 | ## Install 16 | 17 | Pre-requisite: You'll need [Expo](https://expo.dev/) installed on your machine. 18 | 19 | Clone the Repo: `git clone https://github.com/ReactNativeSchool/match-the-pairs-react-native.git` 20 | 21 | Install Depedencies: `yarn install` / `npm install` 22 | 23 | ## Running the App 24 | 25 | - iOS: `yarn ios` / `npm run ios` 26 | - Android: `yarn android` / `npm run android` 27 | - Web: `yarn web` / `npm run web` 28 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "MatchThePairs", 4 | "slug": "MatchThePairs", 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 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactNativeSchool/match-the-pairs-react-native/24cb7fb39b3d92b7541d46f28913e685fe761d7a/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactNativeSchool/match-the-pairs-react-native/24cb7fb39b3d92b7541d46f28913e685fe761d7a/assets/demo.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactNativeSchool/match-the-pairs-react-native/24cb7fb39b3d92b7541d46f28913e685fe761d7a/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactNativeSchool/match-the-pairs-react-native/24cb7fb39b3d92b7541d46f28913e685fe761d7a/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactNativeSchool/match-the-pairs-react-native/24cb7fb39b3d92b7541d46f28913e685fe761d7a/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: [ 6 | "react-native-reanimated/plugin", 7 | [ 8 | "module-resolver", 9 | { 10 | alias: { 11 | // This needs to be mirrored in tsconfig.json 12 | src: "./src", 13 | assets: "./src/assets", 14 | images: "./src/assets/images", 15 | components: "./src/components", 16 | constants: "./src/constants", 17 | context: "./src/context", 18 | hooks: "./src/hooks", 19 | navigation: "./src/navigation", 20 | screens: "./src/screens", 21 | utils: "./src/utils", 22 | }, 23 | }, 24 | ], 25 | ], 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matchthepairs", 3 | "version": "1.0.0", 4 | "main": "node_modules/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 | "eject": "expo eject" 11 | }, 12 | "dependencies": { 13 | "expo": "~45.0.0", 14 | "expo-clipboard": "~3.0.1", 15 | "expo-status-bar": "~1.3.0", 16 | "lodash": "^4.17.21", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "react-native": "0.68.2", 20 | "react-native-reanimated": "~2.8.0", 21 | "react-native-web": "0.17.7" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.12.9", 25 | "@types/lodash": "^4.14.182", 26 | "@types/react": "~17.0.21", 27 | "@types/react-native": "~0.66.13", 28 | "babel-plugin-module-resolver": "^4.1.0", 29 | "typescript": "~4.3.5" 30 | }, 31 | "private": true, 32 | "resolutions": { 33 | "@types/react": "^17" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/images/card-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactNativeSchool/match-the-pairs-react-native/24cb7fb39b3d92b7541d46f28913e685fe761d7a/src/assets/images/card-back.png -------------------------------------------------------------------------------- /src/components/buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Text, 3 | StyleSheet, 4 | ViewStyle, 5 | TextStyle, 6 | Pressable, 7 | } from "react-native"; 8 | 9 | import { Colors, Spacing, Theme } from "constants/index"; 10 | 11 | type ButtonProps = { 12 | children: string; 13 | onPress: () => void; 14 | type?: "primary"; 15 | }; 16 | 17 | export const Button = ({ onPress, children, type }: ButtonProps) => { 18 | const buttonStyles: ViewStyle[] = [styles.button]; 19 | const textStyles: TextStyle[] = [styles.text]; 20 | 21 | if (type === "primary") { 22 | buttonStyles.push(styles.buttonPrimary); 23 | textStyles.push(styles.textPrimary); 24 | } 25 | 26 | return ( 27 | [ 30 | ...buttonStyles, 31 | { opacity: pressed ? 0.75 : 1 }, 32 | ]} 33 | > 34 | {children} 35 | 36 | ); 37 | }; 38 | 39 | const styles = StyleSheet.create({ 40 | button: { 41 | padding: Spacing.md, 42 | paddingHorizontal: Spacing.xl, 43 | backgroundColor: Colors.greyMedium, 44 | margin: Spacing.sm, 45 | borderRadius: Theme.radiusSm, 46 | }, 47 | buttonPrimary: { 48 | backgroundColor: Colors.tealLight, 49 | }, 50 | text: { 51 | color: Colors.greyDarkest, 52 | fontWeight: "500", 53 | fontSize: 16, 54 | }, 55 | textPrimary: { 56 | color: Colors.tealDark, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from "./Button"; 2 | -------------------------------------------------------------------------------- /src/components/cards/GameCard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | StyleSheet, 3 | ViewStyle, 4 | TextStyle, 5 | TouchableOpacity, 6 | Image, 7 | } from "react-native"; 8 | import Animated, { 9 | withTiming, 10 | useAnimatedStyle, 11 | withDelay, 12 | } from "react-native-reanimated"; 13 | 14 | import { Colors, Spacing, Theme } from "constants/index"; 15 | import CardBack from "images/card-back.png"; 16 | 17 | type GameCardProps = { 18 | selected?: boolean; 19 | index: number; 20 | emojis: string[]; 21 | onPress: () => void; 22 | visible: boolean; 23 | disabled: boolean; 24 | }; 25 | 26 | export const GameCard = ({ 27 | index, 28 | emojis, 29 | selected, 30 | onPress, 31 | visible, 32 | disabled, 33 | }: GameCardProps) => { 34 | const cardStyles: ViewStyle[] = [styles.card]; 35 | const textStyles: TextStyle[] = [styles.text]; 36 | 37 | if (selected) { 38 | cardStyles.push(styles.cardSelected); 39 | } 40 | 41 | if (disabled) { 42 | textStyles.push(styles.textDisabled); 43 | } 44 | 45 | // Back = the side of the card initially shown (with the ❓) 46 | const backStyles = useAnimatedStyle(() => { 47 | return { 48 | // if the card is becoming visible we want to immediately start the hide animation of the "back" 49 | // of the card. If we're showing the emoji, we want to wait for this animation to start so that 50 | // the emoji starts to fade away first, thus the delay 51 | opacity: withDelay( 52 | visible ? 0 : 250, 53 | withTiming(visible ? 0 : 1, { 54 | duration: 500, 55 | }) 56 | ), 57 | transform: [ 58 | { 59 | rotateY: withDelay( 60 | visible ? 0 : 250, 61 | withTiming(visible ? "90deg" : "0deg", { duration: 500 }) 62 | ), 63 | }, 64 | ], 65 | }; 66 | }); 67 | 68 | // Front = the side of the card that is shown when the card is clicked. The emoji side 69 | const frontStyles = useAnimatedStyle(() => { 70 | return { 71 | opacity: withDelay( 72 | visible ? 250 : 0, 73 | withTiming(visible ? 1 : 0, { 74 | duration: 500, 75 | }) 76 | ), 77 | transform: [ 78 | { 79 | rotateY: withDelay( 80 | visible ? 250 : 0, 81 | withTiming(visible ? "0deg" : "90deg", { duration: 500 }) 82 | ), 83 | }, 84 | ], 85 | }; 86 | }); 87 | 88 | return ( 89 | 95 | 96 | 97 | 98 | 99 | {emojis[index]} 100 | 101 | 102 | ); 103 | }; 104 | 105 | const styles = StyleSheet.create({ 106 | card: { 107 | flex: 1, 108 | backgroundColor: Colors.greyMedium, 109 | justifyContent: "center", 110 | alignItems: "center", 111 | margin: Spacing.sm, 112 | borderRadius: Theme.radius, 113 | borderWidth: 2, 114 | borderColor: Colors.greyMedium, 115 | }, 116 | cardSelected: { 117 | borderColor: Colors.tealLight, 118 | }, 119 | text: { 120 | fontSize: 35, 121 | position: "absolute", 122 | }, 123 | textDisabled: { 124 | opacity: 0.7, 125 | }, 126 | image: { 127 | width: 50, 128 | height: 50, 129 | tintColor: Colors.greyDark, 130 | }, 131 | }); 132 | -------------------------------------------------------------------------------- /src/components/cards/Stats.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, StyleSheet, Text, ViewStyle } from "react-native"; 3 | import Animated, { 4 | useAnimatedStyle, 5 | withSpring, 6 | } from "react-native-reanimated"; 7 | 8 | import { Colors, Spacing, Theme } from "src/constants"; 9 | 10 | type StatsCardProps = { 11 | title: string; 12 | numerator: number; 13 | denominator?: number; 14 | }; 15 | 16 | export const StatsCard = (props: StatsCardProps) => { 17 | const { numerator, denominator } = props; 18 | const showProgressBar = denominator !== undefined; 19 | 20 | const [cardWidth, setCardWidth] = React.useState(0); 21 | 22 | const progressBarContainerStyles: ViewStyle[] = [styles.progressBarContainer]; 23 | 24 | if (showProgressBar) { 25 | progressBarContainerStyles.push({ backgroundColor: Colors.greyMedium }); 26 | } 27 | 28 | const progressBarWidthAnimated = useAnimatedStyle(() => { 29 | if (!showProgressBar) { 30 | return { 31 | width: 0, 32 | }; 33 | } 34 | 35 | // We clamp at 0 and the last number so that the bar doesn't extend outside of 36 | // the card. If we jump from 8 to 0 (reseting a game) the bar glitches and 37 | // empties, refills, and empties again. Clamping fixes that. 38 | const useClamping = numerator === 0 || numerator >= denominator; 39 | return { 40 | width: withSpring((numerator / denominator) * cardWidth, { 41 | overshootClamping: useClamping, 42 | stiffness: 75, 43 | }), 44 | }; 45 | }, [numerator, denominator, cardWidth]); 46 | 47 | const progressBarStyles: ViewStyle[] = [ 48 | styles.progressBar, 49 | progressBarWidthAnimated, 50 | ]; 51 | 52 | if (numerator === denominator) { 53 | progressBarStyles.push({ borderBottomRightRadius: 0 }); 54 | } 55 | 56 | return ( 57 | setCardWidth(e.nativeEvent.layout.width)} 60 | > 61 | 62 | 63 | 64 | 65 | {props.title} 66 | 67 | {numerator} 68 | {denominator && ( 69 | {`/${denominator}`} 70 | )} 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | const borderRadius = Theme.radius; 78 | const styles = StyleSheet.create({ 79 | container: { 80 | flex: 1, 81 | backgroundColor: Colors.greyLight, 82 | margin: Spacing.sm, 83 | borderRadius, 84 | }, 85 | content: { 86 | padding: Spacing.sm, 87 | }, 88 | title: { 89 | fontWeight: "500", 90 | fontSize: 16, 91 | color: Colors.greyDarkest, 92 | marginBottom: Spacing.xs, 93 | }, 94 | numerator: { 95 | color: Colors.greyDarkest, 96 | fontSize: 20, 97 | fontWeight: "600", 98 | }, 99 | denominator: { 100 | color: Colors.greyDark, 101 | fontSize: 14, 102 | fontWeight: "500", 103 | }, 104 | progressBarContainer: { 105 | backgroundColor: "transparent", 106 | height: 8, 107 | borderTopLeftRadius: borderRadius, 108 | borderTopRightRadius: borderRadius, 109 | marginBottom: Spacing.xs, 110 | }, 111 | progressBar: { 112 | height: 8, 113 | width: 0, 114 | backgroundColor: Colors.blueMedium, 115 | borderTopLeftRadius: borderRadius, 116 | borderTopRightRadius: borderRadius, 117 | borderBottomRightRadius: borderRadius, 118 | }, 119 | }); 120 | -------------------------------------------------------------------------------- /src/components/cards/index.tsx: -------------------------------------------------------------------------------- 1 | export { StatsCard } from "./Stats"; 2 | export { GameCard } from "./GameCard"; 3 | -------------------------------------------------------------------------------- /src/constants/Theme.ts: -------------------------------------------------------------------------------- 1 | export const Theme = { 2 | radiusSm: 4, 3 | radius: 8, 4 | }; 5 | 6 | export const Colors = { 7 | greyLight: "#f9fafa", 8 | greyMedium: "#e8ebed", 9 | greyDark: "#676c6f", 10 | greyDarkest: "#34383c", 11 | 12 | tealLight: "#5cebdf", 13 | tealDark: "#054861", 14 | 15 | blueMedium: "#2451f5", 16 | }; 17 | 18 | export const Spacing = { 19 | xs: 3, 20 | sm: 5, 21 | md: 10, 22 | lg: 15, 23 | xl: 20, 24 | }; 25 | -------------------------------------------------------------------------------- /src/constants/emojis.ts: -------------------------------------------------------------------------------- 1 | export const emojiList = [ 2 | "🤠", 3 | "😈", 4 | "👻", 5 | "🦖", 6 | "🥕", 7 | "📬", 8 | "📸", 9 | "🦦", 10 | "🙀", 11 | "🏄", 12 | "😇", 13 | "🏳️‍🌈", 14 | "💣", 15 | "🕶️", 16 | "🦄", 17 | "🐶", 18 | "🐱", 19 | "🌮", 20 | "🧯", 21 | ]; 22 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Theme"; 2 | export * from "./emojis"; 3 | -------------------------------------------------------------------------------- /src/hooks/useMatchGame.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import _ from "lodash"; 3 | 4 | import { emojiList } from "src/constants"; 5 | 6 | const shuffleEmojis = () => { 7 | // Shuffle the deck of emojis so we get different ones each game then grab the first 8 8 | const shortenedDeck = _.shuffle(emojiList).slice(0, 8); 9 | // Make a deck with 2 of each emoji and then shuffle that 10 | return _.shuffle([...shortenedDeck, ...shortenedDeck]); 11 | }; 12 | 13 | export const useMatchGame = () => { 14 | const [emojis, setEmojis] = useState(shuffleEmojis()); 15 | const [matchedCards, setMatchedCards] = useState([]); 16 | const [comparisonCards, setComparisonCards] = useState([]); 17 | const [totalMoves, setTotalMoves] = useState(0); 18 | 19 | useEffect(() => { 20 | // After they've selected two cards we'll hide them after one second 21 | let timeout: ReturnType; 22 | if (comparisonCards.length == 2) { 23 | timeout = setTimeout(() => { 24 | setComparisonCards([]); 25 | }, 1000); 26 | } 27 | 28 | return () => { 29 | if (timeout) { 30 | clearTimeout(timeout); 31 | } 32 | }; 33 | }, [comparisonCards]); 34 | 35 | const reset = () => { 36 | setMatchedCards([]); 37 | setComparisonCards([]); 38 | setTotalMoves(0); 39 | setTimeout(() => { 40 | // We want to wait to shuffle the emojis otherwise any cards that are "flipped" will show 41 | // the new emoji before it flips over 42 | setEmojis(shuffleEmojis()); 43 | }, 500); 44 | }; 45 | 46 | const chooseCard = (index: number) => { 47 | // we're comparing this card to a previously selected card 48 | if (comparisonCards.length === 1) { 49 | // don't let them choose the same card twice 50 | if (comparisonCards[0] === index) { 51 | return; 52 | } 53 | 54 | // increase move count 55 | setTotalMoves((moves) => moves + 1); 56 | 57 | setComparisonCards((cards) => { 58 | // get the selected cards 59 | const newCards = [...cards, index]; 60 | 61 | // compare the emojis. If they match, update the visible cards 62 | if (emojis[newCards[0]] === emojis[newCards[1]]) { 63 | setMatchedCards((c) => [...c, ...newCards]); 64 | } 65 | 66 | // return the update selected cards so the user can see both at the same time 67 | return newCards; 68 | }); 69 | } else { 70 | // new guess, reset the array 71 | setComparisonCards([index]); 72 | } 73 | }; 74 | 75 | return { 76 | emojis, 77 | reset, 78 | chooseCard, 79 | activeCardIndex: comparisonCards[comparisonCards.length - 1], 80 | matchedCards, 81 | comparisonCards, 82 | totalMoves, 83 | matchCount: matchedCards.length / 2, 84 | totalPairs: emojis.length / 2, 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Screen from "screens/MatchThePairs"; 2 | 3 | export default Screen; 4 | -------------------------------------------------------------------------------- /src/screens/MatchThePairs.tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView, StyleSheet, View, Text } from "react-native"; 2 | import { StatusBar } from "expo-status-bar"; 3 | 4 | import { StatsCard, GameCard } from "components/cards"; 5 | import { Button } from "components/buttons"; 6 | 7 | import { Spacing, Colors } from "constants/index"; 8 | 9 | import { useMatchGame } from "hooks/useMatchGame"; 10 | import { shareGame } from "src/utils/shareGame"; 11 | 12 | const ROWS = [ 13 | [0, 1, 2, 3], 14 | [4, 5, 6, 7], 15 | [8, 9, 10, 11], 16 | [12, 13, 14, 15], 17 | ]; 18 | 19 | const MatchThePairs = () => { 20 | const { 21 | emojis, 22 | reset, 23 | chooseCard, 24 | activeCardIndex, 25 | matchedCards, 26 | comparisonCards, 27 | totalMoves, 28 | matchCount, 29 | totalPairs, 30 | } = useMatchGame(); 31 | 32 | const handlePress = (index: number) => { 33 | chooseCard(index); 34 | }; 35 | 36 | const handleShare = () => { 37 | if (matchCount === totalPairs) { 38 | shareGame({ emojis, moveCount: totalMoves }); 39 | } else { 40 | alert("You haven't matched all the pairs yet!"); 41 | } 42 | }; 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | Match the pairs 🤔 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | {ROWS.map((indices, rowIndex) => ( 62 | 63 | {indices.map((emojiIndex) => { 64 | const inMatchedCard = matchedCards.includes(emojiIndex); 65 | const cardIsVisible = 66 | inMatchedCard || comparisonCards.includes(emojiIndex); 67 | 68 | return ( 69 | handlePress(emojiIndex)} 74 | selected={activeCardIndex === emojiIndex} 75 | visible={cardIsVisible} 76 | disabled={inMatchedCard} 77 | /> 78 | ); 79 | })} 80 | 81 | ))} 82 | 83 | 84 | 87 | 88 | 89 | 90 | 91 | ); 92 | }; 93 | 94 | const styles = StyleSheet.create({ 95 | // container and content are used together to make sure the board doesn't get too wide 96 | // and ensure the board is always centered 97 | container: { 98 | flex: 1, 99 | paddingVertical: Spacing.lg, 100 | alignItems: "center", 101 | }, 102 | content: { 103 | flex: 1, 104 | width: "100%", 105 | maxWidth: 550, 106 | }, 107 | headerText: { 108 | color: Colors.greyDarkest, 109 | fontSize: 20, 110 | fontWeight: "600", 111 | marginTop: Spacing.md, 112 | marginBottom: Spacing.xl, 113 | }, 114 | row: { 115 | flexDirection: "row", 116 | marginHorizontal: Spacing.sm, 117 | }, 118 | gameRow: { 119 | flex: 1, 120 | }, 121 | header: { 122 | marginHorizontal: Spacing.md, 123 | }, 124 | stats: { 125 | marginBottom: Spacing.lg, 126 | }, 127 | actions: { 128 | justifyContent: "center", 129 | marginTop: Spacing.xl, 130 | }, 131 | }); 132 | 133 | export default MatchThePairs; 134 | -------------------------------------------------------------------------------- /src/utils/shareGame.ts: -------------------------------------------------------------------------------- 1 | import { Share, Platform } from "react-native"; 2 | import * as Clipboard from "expo-clipboard"; 3 | 4 | type shareGameProps = { 5 | emojis: string[]; 6 | moveCount: number; 7 | }; 8 | 9 | const buildRow = (emojis: string[], start: number, end: number) => 10 | emojis.slice(start, end).join(" "); 11 | 12 | export const shareGame = async ({ emojis, moveCount }: shareGameProps) => { 13 | const row1 = buildRow(emojis, 0, 4); 14 | const row2 = buildRow(emojis, 4, 8); 15 | const row3 = buildRow(emojis, 8, 12); 16 | const row4 = buildRow(emojis, 12, 16); 17 | 18 | const emojiBoard = [row1, row2, row3, row4].join("\n"); 19 | const message = `I just beat Match the pairs in ${moveCount} moves!\n${emojiBoard}`; 20 | if (Platform.OS === "web") { 21 | Clipboard.setStringAsync(message); 22 | 23 | alert("Copied results to clipboard"); 24 | return; 25 | } 26 | 27 | return Share.share({ 28 | message, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | // This needs to be mirrored in babel.config.js 8 | "src/*": ["src/*"], 9 | "assets/*": ["src/assets/*"], 10 | "images/*": ["src/assets/images/*"], 11 | "components/*": ["src/components/*"], 12 | "constants/*": ["src/constants/*"], 13 | "context/*": ["src/context/*"], 14 | "hooks/*": ["src/hooks/*"], 15 | "navigation/*": ["src/navigation/*"], 16 | "screens/*": ["src/screens/*"], 17 | "utils/*": ["src/utils/*"] 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------