├── .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 | 
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 |
--------------------------------------------------------------------------------