diff --git a/.eslintrc.js b/.eslintrc.js index 583674ec..fa5c0f4c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,19 +12,20 @@ module.exports = { 'prettier', ], rules: { + 'quotes': ['error', 'single'], '@typescript-eslint/no-var-requires': 0, 'jest/no-disabled-tests': 0, 'semi': ['error', 'always'], 'eol-last': ['error', 'always'], 'import/order': ['error', { - "pathGroups": [ + 'pathGroups': [ { - "pattern": "react", - "group": "builtin", - "position": "before" + 'pattern': 'react', + 'group': 'builtin', + 'position': 'before' } ], - "pathGroupsExcludedImportTypes": ["react"], + 'pathGroupsExcludedImportTypes': ['react'], 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'alphabetize': { 'order': 'asc', diff --git a/App.tsx b/App.tsx index 3e99db1c..783d7d91 100644 --- a/App.tsx +++ b/App.tsx @@ -13,7 +13,7 @@ import { AddendModalContextProvider } from './src/components/Sheets/AddendModalC import { GameSheetContextProvider } from './src/components/Sheets/GameSheetContext'; import { Navigation } from './src/Navigation'; -if (process.env.EXPO_PUBLIC_FIREBASE_ANALYTICS == "false") { +if (process.env.EXPO_PUBLIC_FIREBASE_ANALYTICS == 'false') { analytics().setAnalyticsCollectionEnabled(false); } diff --git a/app.config.js b/app.config.js index 34df5408..1c1e901c 100644 --- a/app.config.js +++ b/app.config.js @@ -2,116 +2,116 @@ const variant = process.env.APP_VARIANT; let packageName; switch (variant) { - case "development": - packageName = "com.wyne.scorepad.dev"; + case 'development': + packageName = 'com.wyne.scorepad.dev'; break; - case "preview": - packageName = "com.wyne.scorepad.preview"; + case 'preview': + packageName = 'com.wyne.scorepad.preview'; break; default: - packageName = "com.wyne.scorepad"; + packageName = 'com.wyne.scorepad'; break; } let name; switch (variant) { - case "development": - name = "ScorePad with Rounds (dev)"; + case 'development': + name = 'ScorePad with Rounds (dev)'; break; - case "preview": - name = "ScorePad with Rounds (preview)"; + case 'preview': + name = 'ScorePad with Rounds (preview)'; break; default: - name = "ScorePad with Rounds"; + name = 'ScorePad with Rounds'; break; } let icon; switch (variant) { - case "development": - icon = "./assets/icon-dev.png"; + case 'development': + icon = './assets/icon-dev.png'; break; - case "preview": - icon = "./assets/icon-preview.png"; + case 'preview': + icon = './assets/icon-preview.png'; break; default: - icon = "./assets/icon.png"; + icon = './assets/icon.png'; break; } let androidIcon; let androidIconBg; switch (variant) { - case "development": - androidIcon = "./assets/adaptive-icon-dev.png"; - androidIconBg = "./assets/adaptive-icon-bg-dev.png"; + case 'development': + androidIcon = './assets/adaptive-icon-dev.png'; + androidIconBg = './assets/adaptive-icon-bg-dev.png'; break; - case "preview": - androidIcon = "./assets/adaptive-icon-preview.png"; - androidIconBg = "./assets/adaptive-icon-bg-preview.png"; + case 'preview': + androidIcon = './assets/adaptive-icon-preview.png'; + androidIconBg = './assets/adaptive-icon-bg-preview.png'; break; default: - androidIcon = "./assets/adaptive-icon.png"; - androidIconBg = "./assets/adaptive-icon-bg.png"; + androidIcon = './assets/adaptive-icon.png'; + androidIconBg = './assets/adaptive-icon-bg.png'; break; } export default { name: name, - slug: "scorepad", - version: "2.5.2", - orientation: "default", + slug: 'scorepad', + version: '2.5.2', + orientation: 'default', icon: icon, - assetBundlePatterns: ["assets/*"], - backgroundColor: "#000000", + assetBundlePatterns: ['assets/*'], + backgroundColor: '#000000', ios: { bundleIdentifier: packageName, supportsTablet: true, requireFullScreen: false, - buildNumber: "76", + buildNumber: '76', infoPlist: { RCTAsyncStorageExcludeFromBackup: false, }, - googleServicesFile: "./GoogleService-Info.plist", + googleServicesFile: './GoogleService-Info.plist', }, android: { icon: androidIcon, adaptiveIcon: { - foregroundImage: "./assets/adaptive-icon-fg.png", + foregroundImage: './assets/adaptive-icon-fg.png', backgroundImage: androidIconBg, }, package: packageName, permissions: [], versionCode: 76, - googleServicesFile: "./google-services.json", + googleServicesFile: './google-services.json', }, - userInterfaceStyle: "dark", + userInterfaceStyle: 'dark', web: { - favicon: "./assets/favicon.png", + favicon: './assets/favicon.png', }, extra: { eas: { - projectId: "fc8859ea-b320-41cd-a091-36b3ec7f9b1f", + projectId: 'fc8859ea-b320-41cd-a091-36b3ec7f9b1f', }, }, updates: { fallbackToCacheTimeout: 0, - url: "https://u.expo.dev/fc8859ea-b320-41cd-a091-36b3ec7f9b1f", + url: 'https://u.expo.dev/fc8859ea-b320-41cd-a091-36b3ec7f9b1f', }, runtimeVersion: { - policy: "sdkVersion", + policy: 'sdkVersion', }, - description: "", - githubUrl: "https://github.com/wyne/scorepad-react-native", - owner: "wyne", + description: '', + githubUrl: 'https://github.com/wyne/scorepad-react-native', + owner: 'wyne', plugins: [ - "@react-native-firebase/app", - "@react-native-firebase/crashlytics", + '@react-native-firebase/app', + '@react-native-firebase/crashlytics', [ - "expo-build-properties", + 'expo-build-properties', { ios: { - useFrameworks: "static", + useFrameworks: 'static', }, }, ], diff --git a/jest.config.js b/jest.config.js index 087a4911..9bf4aa37 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,11 +4,11 @@ module.exports = { '^.+\\.tsx?$': 'babel-jest', }, transformIgnorePatterns: [ - "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)" + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)' ], testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], setupFilesAfterEnv: [ - "@testing-library/jest-native/extend-expect" + '@testing-library/jest-native/extend-expect' ] }; diff --git a/redux/GamesSlice.ts b/redux/GamesSlice.ts index 09484f54..7c22d3e6 100644 --- a/redux/GamesSlice.ts +++ b/redux/GamesSlice.ts @@ -1,7 +1,9 @@ import analytics from '@react-native-firebase/analytics'; import { PayloadAction, createAsyncThunk, createEntityAdapter, createSelector, createSlice } from '@reduxjs/toolkit'; +import { getContrastRatio } from 'colorsheet'; import * as Crypto from 'expo-crypto'; +import { getPalette } from '../src/ColorPalette'; import logger from '../src/Logger'; import { ScoreState, playerAdd, selectAllPlayers, selectPlayerById } from './PlayersSlice'; @@ -16,6 +18,7 @@ export interface GameState { roundTotal: number; playerIds: string[]; locked?: boolean; + palette?: string; } const gamesAdapter = createEntityAdapter({ @@ -184,6 +187,28 @@ export const selectSortedPlayers = createSelector( } ); +const selectPaletteName = (state: RootState, gameId: string) => state.games.entities[gameId]?.palette; +const selectPlayerIndex = (_: RootState, __: string, playerIndex: number) => playerIndex; + +export const selectPlayerColors = createSelector( + [selectPaletteName, selectPlayerIndex], + (paletteName, playerIndex) => { + // TODO: Get player color if it exists + + const palette = getPalette(paletteName || 'original'); + + const bg = palette[playerIndex % palette.length]; + + const blackContrast = getContrastRatio(bg, '#000').number; + const whiteContrast = getContrastRatio(bg, '#fff').number; + + // +1 to give a slight preference to white + const fg = blackContrast >= whiteContrast + 1 ? '#000000' : '#FFFFFF'; + + return [bg, fg]; + } +); + export const { updateGame, roundNext, diff --git a/redux/PlayersSlice.ts b/redux/PlayersSlice.ts index cadba39d..457c43fd 100644 --- a/redux/PlayersSlice.ts +++ b/redux/PlayersSlice.ts @@ -7,6 +7,7 @@ export interface ScoreState { id: string; playerName: string; scores: number[]; + color?: string; } const playersAdapter = createEntityAdapter({ diff --git a/redux/SettingsSlice.ts b/redux/SettingsSlice.ts index 5edb2fa4..5ec8f2b1 100644 --- a/redux/SettingsSlice.ts +++ b/redux/SettingsSlice.ts @@ -14,6 +14,7 @@ export interface SettingsState { onboarded: string | undefined; showPointParticles: boolean; showPlayerIndex: boolean; + showColorPalettes?: boolean; interactionType: InteractionType; lastStoreReviewPrompt: number; }; @@ -48,6 +49,9 @@ const settingsSlice = createSlice({ toggleShowPlayerIndex(state) { state.showPlayerIndex = !state.showPlayerIndex; }, + toggleShowColorPalettes(state) { + state.showColorPalettes = !state.showColorPalettes; + }, setMultiplier(state, action: PayloadAction) { state.multiplier = action.payload; }, @@ -80,6 +84,7 @@ export const { setOnboardedVersion, toggleShowPointParticles, toggleShowPlayerIndex, + toggleShowColorPalettes, setInteractionType, setLastStoreReviewPrompt, } = settingsSlice.actions; diff --git a/redux/store.ts b/redux/store.ts index a44e664d..9de2f8f1 100644 --- a/redux/store.ts +++ b/redux/store.ts @@ -1,5 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { configureStore } from "@reduxjs/toolkit"; +import { configureStore } from '@reduxjs/toolkit'; import { persistStore, persistReducer } from 'redux-persist'; import gamesReducer from './GamesSlice'; @@ -19,6 +19,7 @@ const settingsPersistConfig = { 'onboarded', 'showPointParticles', 'showPlayerIndex', + 'showColorPalettes', 'interactionType', 'lastStoreReviewPrompt', ], diff --git a/redux/testStore.ts b/redux/testStore.ts index c617fa7f..1a24829e 100644 --- a/redux/testStore.ts +++ b/redux/testStore.ts @@ -18,9 +18,9 @@ export const exportData = () => { }; export const getPreloadedState = () => { - const playersJson = JSON.parse("{\"ids\":\"[\\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\\",\\\"06e5a92a-602d-4880-a612-893a10767f4e\\\",\\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\\",\\\"af215759-daef-41f8-aad9-97a90c778257\\\",\\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\\",\\\"81c66201-c84f-439a-9240-a1e45e959838\\\",\\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\\",\\\"a897d496-9c80-4294-b71f-6531e54dc08c\\\",\\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\\",\\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\\",\\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\\",\\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\\",\\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\\",\\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\\",\\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\\",\\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\\",\\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\\",\\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\\",\\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\\",\\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\\",\\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\\",\\\"39fbf246-de02-428f-b376-53a9b5eba40e\\\",\\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\\",\\\"d6759b6c-2fef-4521-8400-56abb3edc547\\\",\\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\\",\\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\\"]\",\"entities\":\"{\\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\\":{\\\"id\\\":\\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\\",\\\"playerName\\\":\\\"Rick\\\",\\\"scores\\\":[4,2,1,1]},\\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\\":{\\\"id\\\":\\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\\",\\\"playerName\\\":\\\"Morty\\\",\\\"scores\\\":[2,null,2,1]},\\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\\":{\\\"id\\\":\\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\\",\\\"playerName\\\":\\\"Summer\\\",\\\"scores\\\":[3,1,1,1]},\\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\\":{\\\"id\\\":\\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\\",\\\"playerName\\\":\\\"Beth\\\",\\\"scores\\\":[2,null,1,2]},\\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\\":{\\\"id\\\":\\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\\",\\\"playerName\\\":\\\"Jerry\\\",\\\"scores\\\":[-1,null,1]},\\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\\":{\\\"id\\\":\\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\\",\\\"playerName\\\":\\\"Arthur\\\",\\\"scores\\\":[12,10,2,11]},\\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\\":{\\\"id\\\":\\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\\",\\\"playerName\\\":\\\"Gertrude\\\",\\\"scores\\\":[25,null,20,9]},\\\"06e5a92a-602d-4880-a612-893a10767f4e\\\":{\\\"id\\\":\\\"06e5a92a-602d-4880-a612-893a10767f4e\\\",\\\"playerName\\\":\\\"Erwin\\\",\\\"scores\\\":[8,2,1,31]},\\\"af215759-daef-41f8-aad9-97a90c778257\\\":{\\\"id\\\":\\\"af215759-daef-41f8-aad9-97a90c778257\\\",\\\"playerName\\\":\\\"Maude\\\",\\\"scores\\\":[6,10,2,12]},\\\"a897d496-9c80-4294-b71f-6531e54dc08c\\\":{\\\"id\\\":\\\"a897d496-9c80-4294-b71f-6531e54dc08c\\\",\\\"playerName\\\":\\\"Carmen\\\",\\\"scores\\\":[10,2,1,4]},\\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\\":{\\\"id\\\":\\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\\",\\\"playerName\\\":\\\"Isaac\\\",\\\"scores\\\":[6,3,1,2]},\\\"81c66201-c84f-439a-9240-a1e45e959838\\\":{\\\"id\\\":\\\"81c66201-c84f-439a-9240-a1e45e959838\\\",\\\"playerName\\\":\\\"Penelope\\\",\\\"scores\\\":[-1,10,13,2]},\\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\\":{\\\"id\\\":\\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\\",\\\"playerName\\\":\\\"Ollie\\\",\\\"scores\\\":[7,2,12,3]},\\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\\":{\\\"id\\\":\\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\\",\\\"playerName\\\":\\\"Justin\\\",\\\"scores\\\":[12,4,4,2,5,-2,4]},\\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\\":{\\\"id\\\":\\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\\",\\\"playerName\\\":\\\"Savitri\\\",\\\"scores\\\":[0,30,8]},\\\"39fbf246-de02-428f-b376-53a9b5eba40e\\\":{\\\"id\\\":\\\"39fbf246-de02-428f-b376-53a9b5eba40e\\\",\\\"playerName\\\":\\\"Thetis\\\",\\\"scores\\\":[1,2,14]},\\\"d6759b6c-2fef-4521-8400-56abb3edc547\\\":{\\\"id\\\":\\\"d6759b6c-2fef-4521-8400-56abb3edc547\\\",\\\"playerName\\\":\\\"Lludd\\\",\\\"scores\\\":[3,43,4]},\\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\\":{\\\"id\\\":\\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\\",\\\"playerName\\\":\\\"Maia\\\",\\\"scores\\\":[0,4,8]},\\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\\":{\\\"id\\\":\\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\\",\\\"playerName\\\":\\\"Galateia\\\",\\\"scores\\\":[1,52,4]},\\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\\":{\\\"id\\\":\\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\\",\\\"playerName\\\":\\\"Rashnu\\\",\\\"scores\\\":[0,null,50]},\\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\\":{\\\"id\\\":\\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\\",\\\"playerName\\\":\\\"Iocasta\\\",\\\"scores\\\":[1,50,50]},\\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\\":{\\\"id\\\":\\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\\",\\\"playerName\\\":\\\"Myrddin\\\",\\\"scores\\\":[0,20,4]},\\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\\":{\\\"id\\\":\\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\\",\\\"playerName\\\":\\\"Venere\\\",\\\"scores\\\":[0,10,100]},\\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\\":{\\\"id\\\":\\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\\",\\\"playerName\\\":\\\"Lugos\\\",\\\"scores\\\":[0,50,4]},\\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\\":{\\\"id\\\":\\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\\",\\\"playerName\\\":\\\"Eudora\\\",\\\"scores\\\":[1,10,4]},\\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\\":{\\\"id\\\":\\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\\",\\\"playerName\\\":\\\"Indra\\\",\\\"scores\\\":[1,-10,58]}}\",\"_persist\":\"{\\\"version\\\":0,\\\"rehydrated\\\":true}\"}"); - const settingsJson = JSON.parse("{\"home_fullscreen\":\"false\",\"multiplier\":\"1\",\"addendOne\":\"1\",\"addendTwo\":\"10\",\"currentGameId\":\"\\\"d0ee7acd-da3c-47e9-b535-6bc401f3c16d\\\"\",\"onboarded\":\"\\\"2.4.1\\\"\",\"showPointParticles\":\"true\",\"_persist\":\"{\\\"version\\\":0,\\\"rehydrated\\\":true}\"}"); - const gamesJson = JSON.parse("{\"ids\":\"[\\\"bba7e79a-10d6-4d7d-97d0-1992e34579cd\\\",\\\"9139ee7f-05fa-4152-95f3-62084f94bf85\\\",\\\"46753726-1f11-424e-93c3-2491ab3058bd\\\",\\\"a871776b-578b-415d-932a-dda41145b0ed\\\"]\",\"entities\":\"{\\\"a871776b-578b-415d-932a-dda41145b0ed\\\":{\\\"id\\\":\\\"a871776b-578b-415d-932a-dda41145b0ed\\\",\\\"title\\\":\\\"Family Game Night\\\",\\\"dateCreated\\\":1700389151005,\\\"roundCurrent\\\":3,\\\"roundTotal\\\":4,\\\"playerIds\\\":[\\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\\",\\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\\",\\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\\",\\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\\",\\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\\"]},\\\"46753726-1f11-424e-93c3-2491ab3058bd\\\":{\\\"id\\\":\\\"46753726-1f11-424e-93c3-2491ab3058bd\\\",\\\"title\\\":\\\"Dutch Blitz\\\",\\\"dateCreated\\\":1700448139651,\\\"roundCurrent\\\":3,\\\"roundTotal\\\":4,\\\"playerIds\\\":[\\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\\",\\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\\",\\\"06e5a92a-602d-4880-a612-893a10767f4e\\\",\\\"af215759-daef-41f8-aad9-97a90c778257\\\",\\\"a897d496-9c80-4294-b71f-6531e54dc08c\\\",\\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\\",\\\"81c66201-c84f-439a-9240-a1e45e959838\\\",\\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\\"]},\\\"9139ee7f-05fa-4152-95f3-62084f94bf85\\\":{\\\"id\\\":\\\"9139ee7f-05fa-4152-95f3-62084f94bf85\\\",\\\"title\\\":\\\"Solitaire\\\",\\\"dateCreated\\\":1700448496128,\\\"roundCurrent\\\":6,\\\"roundTotal\\\":7,\\\"playerIds\\\":[\\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\\"]},\\\"bba7e79a-10d6-4d7d-97d0-1992e34579cd\\\":{\\\"id\\\":\\\"bba7e79a-10d6-4d7d-97d0-1992e34579cd\\\",\\\"title\\\":\\\"All In\\\",\\\"dateCreated\\\":1700448546805,\\\"roundCurrent\\\":2,\\\"roundTotal\\\":3,\\\"playerIds\\\":[\\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\\",\\\"39fbf246-de02-428f-b376-53a9b5eba40e\\\",\\\"d6759b6c-2fef-4521-8400-56abb3edc547\\\",\\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\\",\\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\\",\\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\\",\\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\\",\\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\\",\\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\\",\\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\\",\\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\\",\\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\\"]}}\",\"_persist\":\"{\\\"version\\\":0,\\\"rehydrated\\\":true}\"}"); + const playersJson = JSON.parse('{"ids":"[\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\",\\"06e5a92a-602d-4880-a612-893a10767f4e\\",\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\",\\"af215759-daef-41f8-aad9-97a90c778257\\",\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\",\\"81c66201-c84f-439a-9240-a1e45e959838\\",\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\",\\"a897d496-9c80-4294-b71f-6531e54dc08c\\",\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\",\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\",\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\",\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\",\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\",\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\",\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\",\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\",\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\",\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\",\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\",\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\",\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\",\\"39fbf246-de02-428f-b376-53a9b5eba40e\\",\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\",\\"d6759b6c-2fef-4521-8400-56abb3edc547\\",\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\",\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\"]","entities":"{\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\":{\\"id\\":\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\",\\"playerName\\":\\"Rick\\",\\"scores\\":[4,2,1,1]},\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\":{\\"id\\":\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\",\\"playerName\\":\\"Morty\\",\\"scores\\":[2,null,2,1]},\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\":{\\"id\\":\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\",\\"playerName\\":\\"Summer\\",\\"scores\\":[3,1,1,1]},\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\":{\\"id\\":\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\",\\"playerName\\":\\"Beth\\",\\"scores\\":[2,null,1,2]},\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\":{\\"id\\":\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\",\\"playerName\\":\\"Jerry\\",\\"scores\\":[-1,null,1]},\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\":{\\"id\\":\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\",\\"playerName\\":\\"Arthur\\",\\"scores\\":[12,10,2,11]},\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\":{\\"id\\":\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\",\\"playerName\\":\\"Gertrude\\",\\"scores\\":[25,null,20,9]},\\"06e5a92a-602d-4880-a612-893a10767f4e\\":{\\"id\\":\\"06e5a92a-602d-4880-a612-893a10767f4e\\",\\"playerName\\":\\"Erwin\\",\\"scores\\":[8,2,1,31]},\\"af215759-daef-41f8-aad9-97a90c778257\\":{\\"id\\":\\"af215759-daef-41f8-aad9-97a90c778257\\",\\"playerName\\":\\"Maude\\",\\"scores\\":[6,10,2,12]},\\"a897d496-9c80-4294-b71f-6531e54dc08c\\":{\\"id\\":\\"a897d496-9c80-4294-b71f-6531e54dc08c\\",\\"playerName\\":\\"Carmen\\",\\"scores\\":[10,2,1,4]},\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\":{\\"id\\":\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\",\\"playerName\\":\\"Isaac\\",\\"scores\\":[6,3,1,2]},\\"81c66201-c84f-439a-9240-a1e45e959838\\":{\\"id\\":\\"81c66201-c84f-439a-9240-a1e45e959838\\",\\"playerName\\":\\"Penelope\\",\\"scores\\":[-1,10,13,2]},\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\":{\\"id\\":\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\",\\"playerName\\":\\"Ollie\\",\\"scores\\":[7,2,12,3]},\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\":{\\"id\\":\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\",\\"playerName\\":\\"Justin\\",\\"scores\\":[12,4,4,2,5,-2,4]},\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\":{\\"id\\":\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\",\\"playerName\\":\\"Savitri\\",\\"scores\\":[0,30,8]},\\"39fbf246-de02-428f-b376-53a9b5eba40e\\":{\\"id\\":\\"39fbf246-de02-428f-b376-53a9b5eba40e\\",\\"playerName\\":\\"Thetis\\",\\"scores\\":[1,2,14]},\\"d6759b6c-2fef-4521-8400-56abb3edc547\\":{\\"id\\":\\"d6759b6c-2fef-4521-8400-56abb3edc547\\",\\"playerName\\":\\"Lludd\\",\\"scores\\":[3,43,4]},\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\":{\\"id\\":\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\",\\"playerName\\":\\"Maia\\",\\"scores\\":[0,4,8]},\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\":{\\"id\\":\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\",\\"playerName\\":\\"Galateia\\",\\"scores\\":[1,52,4]},\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\":{\\"id\\":\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\",\\"playerName\\":\\"Rashnu\\",\\"scores\\":[0,null,50]},\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\":{\\"id\\":\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\",\\"playerName\\":\\"Iocasta\\",\\"scores\\":[1,50,50]},\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\":{\\"id\\":\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\",\\"playerName\\":\\"Myrddin\\",\\"scores\\":[0,20,4]},\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\":{\\"id\\":\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\",\\"playerName\\":\\"Venere\\",\\"scores\\":[0,10,100]},\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\":{\\"id\\":\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\",\\"playerName\\":\\"Lugos\\",\\"scores\\":[0,50,4]},\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\":{\\"id\\":\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\",\\"playerName\\":\\"Eudora\\",\\"scores\\":[1,10,4]},\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\":{\\"id\\":\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\",\\"playerName\\":\\"Indra\\",\\"scores\\":[1,-10,58]}}","_persist":"{\\"version\\":0,\\"rehydrated\\":true}"}'); + const settingsJson = JSON.parse('{"home_fullscreen":"false","multiplier":"1","addendOne":"1","addendTwo":"10","currentGameId":"\\"d0ee7acd-da3c-47e9-b535-6bc401f3c16d\\"","onboarded":"\\"2.4.1\\"","showPointParticles":"true","_persist":"{\\"version\\":0,\\"rehydrated\\":true}"}'); + const gamesJson = JSON.parse('{"ids":"[\\"bba7e79a-10d6-4d7d-97d0-1992e34579cd\\",\\"9139ee7f-05fa-4152-95f3-62084f94bf85\\",\\"46753726-1f11-424e-93c3-2491ab3058bd\\",\\"a871776b-578b-415d-932a-dda41145b0ed\\"]","entities":"{\\"a871776b-578b-415d-932a-dda41145b0ed\\":{\\"id\\":\\"a871776b-578b-415d-932a-dda41145b0ed\\",\\"title\\":\\"Family Game Night\\",\\"dateCreated\\":1700389151005,\\"roundCurrent\\":3,\\"roundTotal\\":4,\\"playerIds\\":[\\"8a7b194a-47d6-45c2-b4a0-eb5f5f7dd1fd\\",\\"f33b3cfb-3b1f-45b4-ab58-a1248a8e82a3\\",\\"08966cc0-2ae0-4356-8552-4c8c5323b68a\\",\\"caf5409d-3921-4ddd-aa3e-1f7a58c42399\\",\\"b756c49e-bc76-438c-a318-3f9036e3eeb5\\"]},\\"46753726-1f11-424e-93c3-2491ab3058bd\\":{\\"id\\":\\"46753726-1f11-424e-93c3-2491ab3058bd\\",\\"title\\":\\"Dutch Blitz\\",\\"dateCreated\\":1700448139651,\\"roundCurrent\\":3,\\"roundTotal\\":4,\\"playerIds\\":[\\"0e85d2aa-515d-4215-bf2a-d02a8d5b5d18\\",\\"e8f07017-ca0b-474d-b748-bb004f0e47d5\\",\\"06e5a92a-602d-4880-a612-893a10767f4e\\",\\"af215759-daef-41f8-aad9-97a90c778257\\",\\"a897d496-9c80-4294-b71f-6531e54dc08c\\",\\"f900dc3b-1cbf-4a44-a58e-0d6aec863f93\\",\\"81c66201-c84f-439a-9240-a1e45e959838\\",\\"2eff84be-5b21-4827-ac57-4d46d8bd292c\\"]},\\"9139ee7f-05fa-4152-95f3-62084f94bf85\\":{\\"id\\":\\"9139ee7f-05fa-4152-95f3-62084f94bf85\\",\\"title\\":\\"Solitaire\\",\\"dateCreated\\":1700448496128,\\"roundCurrent\\":6,\\"roundTotal\\":7,\\"playerIds\\":[\\"8fd75860-06f2-4d3d-9f2d-62ac0eb674f8\\"]},\\"bba7e79a-10d6-4d7d-97d0-1992e34579cd\\":{\\"id\\":\\"bba7e79a-10d6-4d7d-97d0-1992e34579cd\\",\\"title\\":\\"All In\\",\\"dateCreated\\":1700448546805,\\"roundCurrent\\":2,\\"roundTotal\\":3,\\"playerIds\\":[\\"b52d5dc3-602d-4269-9267-6cf2cfa4d529\\",\\"39fbf246-de02-428f-b376-53a9b5eba40e\\",\\"d6759b6c-2fef-4521-8400-56abb3edc547\\",\\"577a6225-d6e8-4b47-91fb-02e85864b2a2\\",\\"aafa4261-3ca2-4552-ba12-81c4103461d0\\",\\"88a40dca-5930-4a21-9131-78acccc9ea0f\\",\\"3c9a3abc-f2b4-4ef9-b953-24bffebab632\\",\\"3dcfd833-be57-47d3-8dc6-1787c0c1d56b\\",\\"f45ff562-94f3-4a28-8f89-1de17c169cfc\\",\\"d543400f-a60a-4ff9-bc05-40e640ca8c8f\\",\\"12f03866-5f89-43cc-857f-6b3c9760df1a\\",\\"cb5acb92-19a8-40e1-9826-5d98879ab23e\\"]}}","_persist":"{\\"version\\":0,\\"rehydrated\\":true}"}'); const restoredData = { settings: { @@ -39,7 +39,7 @@ export const getPreloadedState = () => { } }; - logger.info("restoredData"); + logger.info('restoredData'); logger.info(restoredData); return restoredData; }; diff --git a/src/ColorPalette.ts b/src/ColorPalette.ts new file mode 100644 index 00000000..154f6388 --- /dev/null +++ b/src/ColorPalette.ts @@ -0,0 +1,116 @@ +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { selectPlayerById, updatePlayer } from '../redux/PlayersSlice'; + +type PaletteType = Record; + +const palettes: PaletteType = { + 'original': [ + '#01497c', + '#c25858', + '#f5c800', + '#275436', + '#dc902c', + '#62516a', + '#755647', + '#925561', + ], + 'pastel': [ + '#f9d5e5', + '#eeac99', + '#e06377', + '#c83349', + '#5b9aa0', + '#d1b59b', + '#8f3e3f', + '#f6416c', + ], + 'dark': [ + '#011627', + '#fdfffc', + '#2ec4b6', + '#e71d36', + '#ff9f1c', + '#f3722c', + ], + 'grey': [ + '#f8f9fa', + '#e9ecef', + '#dee2e6', + '#ced4da', + '#adb5bd', + '#6c757d', + '#495057', + '#343a40', + '#212529', + '#000000', + ], + 'a': [ + '#114b5f', + '#456990', + '#e4fde1', + '#f45b69', + '#6b2737' + ], + 'b': [ + '#c25858', + '#01497c', + ], + 'c': [ + '#88498f', + '#779fa1', + '#e0cba8', + '#ff6542', + '#564154' + ], + 'd': [ + '#f8ffe5', + '#06d6a0', + '#1b9aaa', + '#ef476f', + '#ffc43d' + ], + 'e': [ + '#1f2041', + '#4b3f72', + '#ffc857', + '#119da4', + '#19647e' + ], + 'f': [ + '#fcaa67', + '#b0413e', + '#ffffc7', + '#548687', + '#473335' + ], + 'g': [ + '#ffa400', + '#009ffd', + '#2a2a72', + '#232528', + '#eaf6ff' + ] +}; + +export const getPalettes = (): string[] => { + return Object.keys(palettes); +}; + +export const getPalette = (name: string): string[] => { + return palettes[name]; +}; + +export const setPlayerColor = (playerId: string, color: string) => { + const dispatch = useAppDispatch(); + + const player = useAppSelector(state => selectPlayerById(state, playerId)); + if (typeof player == 'undefined') return; + player.color = color; + + dispatch(updatePlayer({ + id: playerId, + changes: { + color: color, + } + })); +}; diff --git a/src/Navigation.tsx b/src/Navigation.tsx index bf6feaeb..e31078f8 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -10,11 +10,11 @@ import AppInfoHeader from '../src/components/Headers/AppInfoHeader'; import GameHeader from '../src/components/Headers/GameHeader'; import HomeHeader from '../src/components/Headers/HomeHeader'; import SettingsHeader from '../src/components/Headers/SettingsHeader'; -import AppInfoScreen from "../src/screens/AppInfoScreen"; -import GameScreen from "../src/screens/GameScreen"; -import ListScreen from "../src/screens/ListScreen"; +import AppInfoScreen from '../src/screens/AppInfoScreen'; +import GameScreen from '../src/screens/GameScreen'; +import ListScreen from '../src/screens/ListScreen'; import OnboardingScreen from '../src/screens/OnboardingScreen'; -import SettingsScreen from "../src/screens/SettingsScreen"; +import SettingsScreen from '../src/screens/SettingsScreen'; import EditPlayerHeader from './components/Headers/EditPlayerHeader'; import ShareHeader from './components/Headers/ShareHeader'; @@ -71,7 +71,7 @@ export const Navigation = () => { initialParams={{ onboarding: true }} options={{ orientation: 'portrait', - title: "Onboarding", + title: 'Onboarding', headerShown: false, }} /> @@ -89,7 +89,7 @@ export const Navigation = () => { { return ; }, @@ -98,7 +98,7 @@ export const Navigation = () => { { return ; }, @@ -107,7 +107,7 @@ export const Navigation = () => { ({ orientation: 'all', - title: "Settings", + title: 'Settings', header: ({ navigation }) => { return ; }, @@ -116,7 +116,7 @@ export const Navigation = () => { { return ; }, @@ -126,7 +126,7 @@ export const Navigation = () => { initialParams={{ index: 0, playerId: '' }} options={({ route }) => ({ orientation: 'portrait', - title: "Edit Player", + title: 'Edit Player', header: ({ navigation }) => { return ; }, @@ -136,7 +136,7 @@ export const Navigation = () => { initialParams={{ onboarding: false }} options={{ orientation: 'portrait', - title: "Tutorial", + title: 'Tutorial', }} /> diff --git a/src/components/BigButtons/BigButton.tsx b/src/components/BigButtons/BigButton.tsx index 247aba89..a3f4eb08 100644 --- a/src/components/BigButtons/BigButton.tsx +++ b/src/components/BigButtons/BigButton.tsx @@ -1,8 +1,8 @@ -import React from "react"; +import React from 'react'; -import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; -import { Icon } from "react-native-elements"; -import Animated, { FadeIn, FadeOut, Layout } from "react-native-reanimated"; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Icon } from 'react-native-elements'; +import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated'; interface Props { onPress: () => void; diff --git a/src/components/Boards/FlexboxBoard.tsx b/src/components/Boards/FlexboxBoard.tsx index da7ce478..43578cfb 100644 --- a/src/components/Boards/FlexboxBoard.tsx +++ b/src/components/Boards/FlexboxBoard.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { getContrastRatio } from 'colorsheet'; import { LayoutChangeEvent, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -20,7 +19,6 @@ const FlexboxBoard: React.FC = () => { if (playerIds == null || playerIds.length == 0) return null; - const palette = ["01497c", "c25858", "f5c800", "275436", "dc902c", "62516a", "755647", "925561"]; const [rows, setRows] = useState(0); const [cols, setCols] = useState(0); @@ -98,8 +96,6 @@ const FlexboxBoard: React.FC = () => { 7 ? "#000000" : "#FFFFFF"} cols={cols} rows={rows} width={calculateTileDimensions(rows, cols).width} diff --git a/src/components/Boards/FlexboxTile.tsx b/src/components/Boards/FlexboxTile.tsx index 2258b327..8723e13d 100644 --- a/src/components/Boards/FlexboxTile.tsx +++ b/src/components/Boards/FlexboxTile.tsx @@ -3,8 +3,9 @@ import React from 'react'; import { DimensionValue, StyleSheet } from 'react-native'; import Animated, { Easing, FadeIn } from 'react-native-reanimated'; +import { selectPlayerColors } from '../../../redux/GamesSlice'; import { useAppSelector } from '../../../redux/hooks'; -import { selectInteractionType } from '../../../redux/selectors'; +import { selectCurrentGame, selectInteractionType } from '../../../redux/selectors'; import { interactionComponents } from '../Interactions/InteractionComponents'; import { InteractionType } from '../Interactions/InteractionType'; import AdditionTile from '../PlayerTiles/AdditionTile/AdditionTile'; @@ -13,8 +14,6 @@ import PlayerIndexLabel from '../PlayerTiles/PlayerIndexLabel'; interface Props { index: number; playerId: string; - color: string; - fontColor: string; cols: number; rows: number; width: number; @@ -23,8 +22,6 @@ interface Props { const FlexboxTile: React.FunctionComponent = ({ index, - color, - fontColor, width, height, cols, @@ -35,7 +32,10 @@ const FlexboxTile: React.FunctionComponent = ({ if (!(width > 0 && height > 0)) return null; if (Number.isNaN(width) || Number.isNaN(height)) return null; + const currentGameId = useAppSelector(state => selectCurrentGame(state)?.id); const playerIndexLabel = useAppSelector(state => state.settings.showPlayerIndex); + const playerColors = useAppSelector(state => selectPlayerColors(state, currentGameId || '', index || 0)); + const [bg, fg] = playerColors; const widthPerc: DimensionValue = `${(100 / cols)}%`; const heightPerc: DimensionValue = `${(100 / rows)}%`; @@ -50,16 +50,16 @@ const FlexboxTile: React.FunctionComponent = ({ style={[ styles.playerCard, { - backgroundColor: color, + backgroundColor: bg, width: widthPerc, height: heightPerc, borderBottomLeftRadius: playerIndexLabel ? 7 : undefined, }]}> - - + + { const navigation = useNavigationMock(); it.skip('should navigate to Game screen when pressed', async () => { - const { getByRole } = render(); + const { getByRole } = render(); const button = getByRole('button'); await waitFor(() => { fireEvent.press(button); - expect(navigation.navigate).toHaveBeenCalledWith("Game"); + expect(navigation.navigate).toHaveBeenCalledWith('Game'); }); }); it.skip('should navigate back a screen when pressed', async () => { - const { getByRole } = render(); + const { getByRole } = render(); const button = getByRole('button'); await waitFor(() => { fireEvent.press(button); diff --git a/src/components/Buttons/CheckButton.tsx b/src/components/Buttons/CheckButton.tsx index b51ce41d..b41c3d64 100644 --- a/src/components/Buttons/CheckButton.tsx +++ b/src/components/Buttons/CheckButton.tsx @@ -25,8 +25,9 @@ const CheckButton: React.FunctionComponent = ({ navigation, route }) => { { await analytics().logEvent('save_game'); if (route?.params?.reason === 'new_game') { - navigation.navigate("Game"); + navigation.navigate('Game'); } else { + //TODO: when the game is first created, this will go back instead of to game screen navigation.goBack(); } }}> diff --git a/src/components/Buttons/HomeButton.tsx b/src/components/Buttons/HomeButton.tsx index 4bf73483..8c4774a8 100644 --- a/src/components/Buttons/HomeButton.tsx +++ b/src/components/Buttons/HomeButton.tsx @@ -50,7 +50,7 @@ const HomeButton: React.FunctionComponent = ({ navigation }) => { return ( { - navigation.navigate("List"); + navigation.navigate('List'); await analytics().logEvent('menu'); storePrompt(); diff --git a/src/components/Buttons/NewGameButton.tsx b/src/components/Buttons/NewGameButton.tsx index 26c28053..84019b11 100644 --- a/src/components/Buttons/NewGameButton.tsx +++ b/src/components/Buttons/NewGameButton.tsx @@ -23,7 +23,7 @@ const NewGameButton: React.FunctionComponent = ({ navigation }) => { const menuActions: MenuAction[] = playerNumberOptions.map((number) => { return { id: number.toString(), - title: number.toString() + (number == 1 ? " Player" : " Players"), + title: number.toString() + (number == 1 ? ' Player' : ' Players'), }; }); @@ -35,7 +35,7 @@ const NewGameButton: React.FunctionComponent = ({ navigation }) => { }) ).then(() => { setTimeout(() => { - navigation.navigate("Settings", { reason: 'new_game' }); + navigation.navigate('Settings', { reason: 'new_game' }); }, 500); }); }; diff --git a/src/components/ColorPalettes/PalettePreview.tsx b/src/components/ColorPalettes/PalettePreview.tsx new file mode 100644 index 00000000..789c71a7 --- /dev/null +++ b/src/components/ColorPalettes/PalettePreview.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { View } from 'react-native'; + +interface Props { + colors: string[]; + selected?: boolean; +} + +const PalettePreview: React.FunctionComponent = ({ colors, selected }) => { + const numColumns = Math.ceil(Math.sqrt(colors.length)); + const numRows = Math.ceil(colors.length / numColumns); + + return ( + + {colors.map((color, index) => ( + + + + + ))} + + ); +}; + +export default PalettePreview; diff --git a/src/components/ColorPalettes/PaletteSelector.tsx b/src/components/ColorPalettes/PaletteSelector.tsx new file mode 100644 index 00000000..912485ad --- /dev/null +++ b/src/components/ColorPalettes/PaletteSelector.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import { ScrollView, StyleSheet, TouchableOpacity } from 'react-native'; + +import { updateGame } from '../../../redux/GamesSlice'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; +import { selectCurrentGame } from '../../../redux/selectors'; +import { getPalette, getPalettes } from '../../ColorPalette'; + +import PalettePreview from './PalettePreview'; + +const MemoizedColorCircle = React.memo(PalettePreview); + +const PaletteSelector: React.FunctionComponent = () => { + const colorPalettes = getPalettes(); + const currentGameId = useAppSelector(state => selectCurrentGame(state)?.id); + const currentPalette = useAppSelector(state => selectCurrentGame(state)?.palette); + + const dispatch = useAppDispatch(); + + if (!currentGameId) return null; + + const onSelect = (palette: string) => { + dispatch(updateGame({ + id: currentGameId, + changes: { + palette: palette, + } + })); + }; + + return ( + + {colorPalettes.map((palette, index) => ( + onSelect(palette)} + > + + + ))} + + ); +}; + +export default PaletteSelector; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + marginVertical: 20, + }, + palette: { + width: 40, + height: 40, + borderWidth: 2, + borderColor: 'transparent', + marginHorizontal: 5, + }, +}); diff --git a/src/components/EditGame.tsx b/src/components/EditGame.tsx index 8b2ea3f4..d375df9e 100644 --- a/src/components/EditGame.tsx +++ b/src/components/EditGame.tsx @@ -7,7 +7,7 @@ import { updateGame } from '../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { selectCurrentGame } from '../../redux/selectors'; -const UNTITLED = "Untitled"; +const UNTITLED = 'Untitled'; const EditGame = ({ }) => { const dispatch = useAppDispatch(); @@ -20,7 +20,7 @@ const EditGame = ({ }) => { const onEndEditingHandler = (e: NativeSyntheticEvent) => { const text = e.nativeEvent.text; - if (text == "") { + if (text == '') { setLocalTitle(UNTITLED); saveGameTitle(UNTITLED); } else { @@ -29,7 +29,7 @@ const EditGame = ({ }) => { }; const onChangeTextHandler = (text: string) => { - if (text == "") { + if (text == '') { saveGameTitle(UNTITLED); } else { saveGameTitle(text); @@ -43,7 +43,7 @@ const EditGame = ({ }) => { dispatch(updateGame({ id: currentGame.id, changes: { - title: title == "" ? UNTITLED : title, + title: title == '' ? UNTITLED : title, } })); }; diff --git a/src/components/GameListItem.tsx b/src/components/GameListItem.tsx index e61037a7..023991cb 100644 --- a/src/components/GameListItem.tsx +++ b/src/components/GameListItem.tsx @@ -42,7 +42,7 @@ const GameListItem: React.FunctionComponent = ({ navigation, gameId, inde */ const chooseGameHandler = async () => { setCurrentGameCallback(); - navigation.navigate("Game"); + navigation.navigate('Game'); await analytics().logEvent('select_game', { index: index, diff --git a/src/components/GameListItemPlayerName.tsx b/src/components/GameListItemPlayerName.tsx index 1a15a712..0a8167a1 100644 --- a/src/components/GameListItemPlayerName.tsx +++ b/src/components/GameListItemPlayerName.tsx @@ -1,7 +1,7 @@ -import { Text } from "react-native"; +import { Text } from 'react-native'; -import { useAppSelector } from "../../redux/hooks"; -import { selectPlayerById } from "../../redux/PlayersSlice"; +import { useAppSelector } from '../../redux/hooks'; +import { selectPlayerById } from '../../redux/PlayersSlice'; interface Props { playerId: string; diff --git a/src/components/Interactions/InteractionComponents.ts b/src/components/Interactions/InteractionComponents.ts index e732af29..7147fee7 100644 --- a/src/components/Interactions/InteractionComponents.ts +++ b/src/components/Interactions/InteractionComponents.ts @@ -1,6 +1,6 @@ -import HalfTap from "./HalfTap/HalfTap"; -import { InteractionType } from "./InteractionType"; -import Swipe from "./Swipe/Swipe"; +import HalfTap from './HalfTap/HalfTap'; +import { InteractionType } from './InteractionType'; +import Swipe from './Swipe/Swipe'; export const interactionComponents = { [InteractionType.HalfTap]: HalfTap, diff --git a/src/components/Interactions/InteractionSelector.tsx b/src/components/Interactions/InteractionSelector.tsx index 9676fa66..c335520e 100644 --- a/src/components/Interactions/InteractionSelector.tsx +++ b/src/components/Interactions/InteractionSelector.tsx @@ -22,9 +22,9 @@ const InteractionSelector: React.FunctionComponent = ( const description = (() => { switch (interactionType) { case InteractionType.HalfTap: - return "Tap the top or bottom of each player's tile."; + return 'Tap the top or bottom of each player\'s tile.'; case InteractionType.SwipeVertical: - return "Swipe up or down on the player's tile."; + return 'Swipe up or down on the player\'s tile.'; } })(); @@ -39,8 +39,8 @@ const InteractionSelector: React.FunctionComponent = ( dispatch(setInteractionType(InteractionType.HalfTap)); }} text="Tap" - icon={} - color={interactionType == InteractionType.HalfTap ? "white" : "grey"} + icon={} + color={interactionType == InteractionType.HalfTap ? 'white' : 'grey'} /> @@ -50,8 +50,8 @@ const InteractionSelector: React.FunctionComponent = ( dispatch(setInteractionType(InteractionType.SwipeVertical)); }} text="Swipe" - icon={} - color={interactionType == InteractionType.SwipeVertical ? "white" : "grey"} + icon={} + color={interactionType == InteractionType.SwipeVertical ? 'white' : 'grey'} /> diff --git a/src/components/Onboarding/Onboarding.test.ts b/src/components/Onboarding/Onboarding.test.ts index 37408d16..a1ac768b 100644 --- a/src/components/Onboarding/Onboarding.test.ts +++ b/src/components/Onboarding/Onboarding.test.ts @@ -1,6 +1,6 @@ -import { SemVer } from "semver"; +import { SemVer } from 'semver'; -import { getOnboardingSemVer } from "./Onboarding"; +import { getOnboardingSemVer } from './Onboarding'; describe('onboarding', () => { it('should return the default if onboarded to 0.0.0', () => { diff --git a/src/components/Onboarding/Onboarding.ts b/src/components/Onboarding/Onboarding.ts index 8a2e622d..75e09f02 100644 --- a/src/components/Onboarding/Onboarding.ts +++ b/src/components/Onboarding/Onboarding.ts @@ -23,7 +23,7 @@ type OnboardingScreens = Record; const onboardingScreens: OnboardingScreens = { '2.2.2': [ { - title: "ScorePad\nwith Rounds", + title: 'ScorePad\nwith Rounds', media: { type: 'image', source: require('../../../assets/icon.png'), @@ -36,7 +36,7 @@ const onboardingScreens: OnboardingScreens = { color: 'rgba(0,0,0,0.6)' }, { - title: "Swipe for points", + title: 'Swipe for points', media: { type: 'video', source: require('../../../assets/video/swipe-gesture.mp4'), @@ -45,7 +45,7 @@ const onboardingScreens: OnboardingScreens = { backgroundColor: '#a0c99a', }, { - title: "Hold for more", + title: 'Hold for more', media: { type: 'video', source: require('../../../assets/video/swipe-powerhold.mp4'), @@ -54,7 +54,7 @@ const onboardingScreens: OnboardingScreens = { backgroundColor: '#d29898', }, { - title: "Change Gestures", + title: 'Change Gestures', media: { type: 'video', source: require('../../../assets/video/gesture-select.mp4'), @@ -63,7 +63,7 @@ const onboardingScreens: OnboardingScreens = { backgroundColor: '#9896c5', }, { - title: "Score History", + title: 'Score History', media: { type: 'image', source: require('../../../assets/onboarding/sheet.png'), @@ -74,7 +74,7 @@ const onboardingScreens: OnboardingScreens = { ], '2.5.0': [ { - title: "New Default:\nSwipe Gestures", + title: 'New Default:\nSwipe Gestures', media: { type: 'video', source: require('../../../assets/video/swipe-gesture.mp4'), @@ -83,7 +83,7 @@ const onboardingScreens: OnboardingScreens = { backgroundColor: '#a0c99a', }, { - title: "Hold for more", + title: 'Hold for more', media: { type: 'video', source: require('../../../assets/video/swipe-powerhold.mp4'), @@ -92,7 +92,7 @@ const onboardingScreens: OnboardingScreens = { backgroundColor: '#d29898', }, { - title: "Change Gestures", + title: 'Change Gestures', media: { type: 'video', source: require('../../../assets/video/gesture-select.mp4'), @@ -104,7 +104,7 @@ const onboardingScreens: OnboardingScreens = { }; const finalScreen: OnboardingScreenItem[] = [{ - title: "That's it!", + title: 'That\'s it!', media: { type: 'image', source: require('../../../assets/icon.png'), diff --git a/src/components/PlayerListItem.tsx b/src/components/PlayerListItem.tsx index c4f7219c..ed03e192 100644 --- a/src/components/PlayerListItem.tsx +++ b/src/components/PlayerListItem.tsx @@ -6,11 +6,10 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Icon, ListItem } from 'react-native-elements'; -import { updateGame } from '../../redux/GamesSlice'; +import { selectGameById, selectPlayerColors, updateGame } from '../../redux/GamesSlice'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { removePlayer, selectPlayerById } from '../../redux/PlayersSlice'; import { selectCurrentGame } from '../../redux/selectors'; -import { palette } from '../constants'; interface Props { playerId: string; @@ -29,25 +28,28 @@ const PlayerListItem: React.FunctionComponent = ({ navigation, edit, }) => { - const currentGame = useAppSelector(selectCurrentGame); + const currentGameId = useAppSelector(state => selectCurrentGame(state)?.id); + const playerIds = useAppSelector(state => selectGameById(state, currentGameId || '')?.playerIds); const player = useAppSelector(state => selectPlayerById(state, playerId)); + const playerColors = useAppSelector(state => selectPlayerColors(state, currentGameId || '', index || 0)); - if (typeof currentGame == 'undefined') return null; + if (currentGameId == '' || currentGameId === undefined) return null; + if (playerIds === undefined) return null; if (typeof index == 'undefined') return null; const dispatch = useAppDispatch(); const deleteConfirmHandler = async () => { Alert.alert( - "Delete Player", - "Are you sure you want to delete this player? This will delete all scores for this player.", + 'Delete Player', + 'Are you sure you want to delete this player? This will delete all scores for this player.', [ { - text: "Cancel", - style: "cancel", + text: 'Cancel', + style: 'cancel', }, { - text: "Delete", + text: 'Delete', onPress: () => { deleteHandler(); } @@ -60,16 +62,16 @@ const PlayerListItem: React.FunctionComponent = ({ removePlayerHandler(); await analytics().logEvent('remove_player', { - game_id: currentGame.id, + game_id: currentGameId, player_index: index, }); }; const removePlayerHandler = () => { dispatch(updateGame({ - id: currentGame.id, + id: currentGameId, changes: { - playerIds: currentGame.playerIds.filter((id) => id != playerId), + playerIds: playerIds.filter((id) => id != playerId), } })); dispatch(removePlayer(playerId)); @@ -96,7 +98,7 @@ const PlayerListItem: React.FunctionComponent = ({ style={[{ flexDirection: 'row', alignItems: 'center', - }]} > + }]}> {index + 1} @@ -104,7 +106,7 @@ const PlayerListItem: React.FunctionComponent = ({ @@ -138,7 +140,7 @@ const styles = StyleSheet.create({ color: '#eee', fontSize: 25, fontVariant: ['tabular-nums'], - fontWeight: "bold", + fontWeight: 'bold', padding: 5, }, colorBadge: { diff --git a/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx b/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx index f294dcfa..629c6e5b 100644 --- a/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx +++ b/src/components/PlayerTiles/AdditionTile/ScoreRound.tsx @@ -46,8 +46,8 @@ const ScoreRound: React.FunctionComponent = ({ containerWidth, roundScore color: fontColor, opacity: scoreMathOpacity }]}> - {roundScore > 0 && " + "} - {roundScore < 0 && " - "} + {roundScore > 0 && ' + '} + {roundScore < 0 && ' - '} {Math.abs(d)} diff --git a/src/components/PopupMenu/AbstractPopupMenu.tsx b/src/components/PopupMenu/AbstractPopupMenu.tsx index 73dc9c90..f8e54ff8 100644 --- a/src/components/PopupMenu/AbstractPopupMenu.tsx +++ b/src/components/PopupMenu/AbstractPopupMenu.tsx @@ -34,7 +34,7 @@ const AbstractPopupMenu: React.FC = (props) => { */ const shareGameHandler = async () => { props.setCurrentGameCallback(); - props.navigation.navigate("Share"); + props.navigation.navigate('Share'); await analytics().logEvent('menu_share', { round_count: roundTotal, @@ -47,7 +47,7 @@ const AbstractPopupMenu: React.FC = (props) => { */ const editGameHandler = async () => { props.setCurrentGameCallback(); - props.navigation.navigate("Settings", { reason: 'edit_game' }); + props.navigation.navigate('Settings', { reason: 'edit_game' }); await analytics().logEvent('menu_edit', { round_count: roundTotal, @@ -63,7 +63,7 @@ const AbstractPopupMenu: React.FC = (props) => { asyncRematchGame({ gameId: props.gameId }) ).then(() => { setTimeout(() => { - props.navigation.navigate("Game"); + props.navigation.navigate('Game'); }, 500); }); }; diff --git a/src/components/PopupMenu/IOSPopupMenu.tsx b/src/components/PopupMenu/IOSPopupMenu.tsx index 8e3c6426..1faaff55 100644 --- a/src/components/PopupMenu/IOSPopupMenu.tsx +++ b/src/components/PopupMenu/IOSPopupMenu.tsx @@ -52,7 +52,7 @@ const IOSPopupMenu: React.FC = ({ }, { id: 'delete', - title: `Delete`, + title: 'Delete', attributes: { destructive: true, }, diff --git a/src/components/Rounds.tsx b/src/components/Rounds.tsx index 0a3b98fc..b7d2701a 100644 --- a/src/components/Rounds.tsx +++ b/src/components/Rounds.tsx @@ -53,7 +53,7 @@ const Rounds: React.FunctionComponent = ({ }) => { roundsScrollViewEl.current.scrollTo({ x: offset, - animated: Platform.OS == "ios" ? true : false + animated: Platform.OS == 'ios' ? true : false }); }, [roundCurrent, roundScollOffset]); @@ -81,7 +81,6 @@ const Rounds: React.FunctionComponent = ({ }) => { const styles = StyleSheet.create({ scoreTableContainer: { flexDirection: 'row', - paddingBottom: 10, } }); diff --git a/src/components/ScoreLog/PlayerNameColumn.tsx b/src/components/ScoreLog/PlayerNameColumn.tsx index ac47862b..c966c6bf 100644 --- a/src/components/ScoreLog/PlayerNameColumn.tsx +++ b/src/components/ScoreLog/PlayerNameColumn.tsx @@ -1,27 +1,35 @@ import React from 'react'; -import { createSelector } from '@reduxjs/toolkit'; import { StyleSheet, Text, View } from 'react-native'; import { Icon } from 'react-native-elements/dist/icons/Icon'; +import { selectPlayerColors } from '../../../redux/GamesSlice'; import { useAppSelector } from '../../../redux/hooks'; +import { selectPlayerById } from '../../../redux/PlayersSlice'; import { selectCurrentGame } from '../../../redux/selectors'; -import { RootState } from '../../../redux/store'; -import { palette, systemBlue } from '../../constants'; +import { systemBlue } from '../../constants'; -const selectPlayerEntities = (state: RootState) => state.players.entities; +interface CellProps { + index: number; + playerId: string; +} -const selectPlayerIds = (_: RootState, playerIds: string[]) => playerIds; +const PlayerNameCell: React.FunctionComponent = ({ index, playerId }) => { + const currentGameId = useAppSelector(state => selectCurrentGame(state)?.id); + const playerColors = useAppSelector(state => selectPlayerColors(state, currentGameId || '', index || 0)); + const playerName = useAppSelector(state => selectPlayerById(state, playerId)?.playerName); -const selectPlayerNamesByIds = createSelector( - [selectPlayerEntities, selectPlayerIds], - (players, playerIds) => playerIds.map(id => players[id]?.playerName) -); + return ( + + {playerName} + + ); +}; const PlayerNameColumn: React.FunctionComponent = () => { const playerIds = useAppSelector(state => selectCurrentGame(state)?.playerIds) || []; - const playerNames = useAppSelector(state => selectPlayerNamesByIds(state, playerIds)); - return ( @@ -32,12 +40,8 @@ const PlayerNameColumn: React.FunctionComponent = () => { size={19} color='white' /> - {playerNames.map((name, index) => ( - - {name} - + {playerIds.map((playerId, index) => ( + ))} ); diff --git a/src/components/Sheets/GameSheet.tsx b/src/components/Sheets/GameSheet.tsx index 7c62aa4b..e7eab6a9 100644 --- a/src/components/Sheets/GameSheet.tsx +++ b/src/components/Sheets/GameSheet.tsx @@ -13,6 +13,7 @@ import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { updatePlayer } from '../../../redux/PlayersSlice'; import { systemBlue } from '../../constants'; import BigButton from '../BigButtons/BigButton'; +import PaletteSelector from '../ColorPalettes/PaletteSelector'; import RematchIcon from '../Icons/RematchIcon'; import Rounds from '../Rounds'; @@ -64,15 +65,15 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight */ const resetGameHandler = () => { Alert.alert( - "Reset Game", - "Warning: This will reset all scores and rounds for this game. Are you sure you want to reset?", + 'Reset Game', + 'Warning: This will reset all scores and rounds for this game. Are you sure you want to reset?', [ { - text: "Cancel", - style: "cancel" + text: 'Cancel', + style: 'cancel' }, { - text: "Reset", + text: 'Reset', onPress: () => { if (currentGameId == undefined) return; if (playerIds == undefined) return; @@ -93,7 +94,7 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight roundTotal: 1, } })); - navigation.navigate("Game"); + navigation.navigate('Game'); } } ] @@ -105,21 +106,21 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight */ const rematchGameHandler = async () => { Alert.alert( - "Rematch", - "This will create a new game with the same players and empty scores.", + 'Rematch', + 'This will create a new game with the same players and empty scores.', [ { - text: "Cancel", - style: "cancel" + text: 'Cancel', + style: 'cancel' }, { - text: "Rematch", + text: 'Rematch', onPress: () => { dispatch( asyncRematchGame({ gameId: currentGameId }) ).then(() => { setTimeout(() => { - navigation.navigate("Game"); + navigation.navigate('Game'); }, 500); }); } @@ -189,6 +190,8 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight [] ); + const showColorPalettes = useAppSelector(state => state.settings.showColorPalettes); + return ( = ({ navigation, containerHeight - - Tap on a column to set the current round. - + + {showColorPalettes && + + } @@ -262,9 +266,9 @@ const GameSheet: React.FunctionComponent = ({ navigation, containerHeight /> } - diff --git a/src/constants.ts b/src/constants.ts index e239a644..5437c669 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,14 +1,4 @@ export const systemBlue = '#0a84ff'; -export const palette = [ - "01497c", - "c25858", - "f5c800", - "275436", - "dc902c", - "62516a", - "755647", - "925561" -]; export const STORAGE_KEY = { GAMES_LIST: '@games_list', }; diff --git a/src/screens/AppInfoScreen.tsx b/src/screens/AppInfoScreen.tsx index 3c745f44..4d0cf8a5 100644 --- a/src/screens/AppInfoScreen.tsx +++ b/src/screens/AppInfoScreen.tsx @@ -8,7 +8,7 @@ import { Alert, Linking, Platform, ScrollView, StyleSheet, Switch, Text, View } import { Button } from 'react-native-elements'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; -import { toggleShowPlayerIndex, toggleShowPointParticles } from '../../redux/SettingsSlice'; +import { toggleShowColorPalettes, toggleShowPlayerIndex, toggleShowPointParticles } from '../../redux/SettingsSlice'; import RotatingIcon from '../components/AppInfo/RotatingIcon'; interface Props { @@ -41,13 +41,15 @@ const AppInfoScreen: React.FunctionComponent = ({ navigation }) => { const showPointParticles = useAppSelector(state => state.settings.showPointParticles); const showPlayerIndex = useAppSelector(state => state.settings.showPlayerIndex); + const showColorPalettes = useAppSelector(state => state.settings.showColorPalettes); const dispatch = useAppDispatch(); const toggleParticleSwitch = () => { dispatch(toggleShowPointParticles()); }; const togglePlayerIndexSwitch = () => { dispatch(toggleShowPlayerIndex()); }; + const toggleColorPalettesSwitch = () => { dispatch(toggleShowColorPalettes()); }; const alertWithVersion = async () => { - Alert.alert(`ScorePad with Rounds\n` + + Alert.alert('ScorePad with Rounds\n' + `v${appVersion} (${buildNumber})\n` + `${Platform.OS} ${Platform.Version}\n` + (process.env.EXPO_PUBLIC_FIREBASE_ANALYTICS) @@ -76,6 +78,10 @@ const AppInfoScreen: React.FunctionComponent = ({ navigation }) => { + + + +
diff --git a/src/screens/EditPlayerScreen.tsx b/src/screens/EditPlayerScreen.tsx index 36c95313..da4ab4f1 100644 --- a/src/screens/EditPlayerScreen.tsx +++ b/src/screens/EditPlayerScreen.tsx @@ -3,13 +3,12 @@ import React, { useState } from 'react'; import analytics from '@react-native-firebase/analytics'; import { ParamListBase, RouteProp } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { NativeSyntheticEvent, ScrollView, StyleSheet, Text, TextInput, TextInputEndEditingEventData, View } from 'react-native'; +import { NativeSyntheticEvent, ScrollView, StyleSheet, TextInput, TextInputEndEditingEventData, View } from 'react-native'; import { Input } from 'react-native-elements'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { updatePlayer } from '../../redux/PlayersSlice'; import { selectCurrentGame } from '../../redux/selectors'; -import { palette } from '../constants'; type RouteParams = { EditPlayer: { @@ -38,8 +37,8 @@ const EditPlayerScreen: React.FC = ({ } }); - const [originalPlayerName] = useState(player?.playerName || ""); - const [localPlayerName, setLocalPlayerName] = useState(player?.playerName || ""); + const [originalPlayerName] = useState(player?.playerName || ''); + const [localPlayerName, setLocalPlayerName] = useState(player?.playerName || ''); if (playerId == null) { return null; } if (player == null) return null; @@ -49,7 +48,7 @@ const EditPlayerScreen: React.FC = ({ const onEndEditingHandler = (e: NativeSyntheticEvent) => { const text = e.nativeEvent.text; - if (text == "") { + if (text == '') { setLocalPlayerName(originalPlayerName); savePlayerName(originalPlayerName); } else { @@ -58,7 +57,7 @@ const EditPlayerScreen: React.FC = ({ }; const onChangeHandler = (text: string) => { - if (text == "") { + if (text == '') { savePlayerName(originalPlayerName); } else { savePlayerName(text); @@ -117,33 +116,8 @@ const EditPlayerScreen: React.FC = ({ value={localPlayerName} /> - - { - palette.map((color, i) => ( - - )) - } - - - Local State: {localPlayerName} - Player State: {player.playerName} - ); }; @@ -166,7 +140,7 @@ const styles = StyleSheet.create({ color: '#eee', fontSize: 25, fontVariant: ['tabular-nums'], - fontWeight: "bold", + fontWeight: 'bold', padding: 5, }, colorBadge: { diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index 26144082..207c6e82 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -11,7 +11,7 @@ import { View, ViewToken, } from 'react-native'; -import { ExpandingDot } from "react-native-animated-pagination-dots"; +import { ExpandingDot } from 'react-native-animated-pagination-dots'; import { Button } from 'react-native-elements'; import Animated, { FadeIn } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 123900c1..633ed20c 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import analytics from '@react-native-firebase/analytics'; import { ParamListBase, RouteProp } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { createSelector } from '@reduxjs/toolkit'; import * as Crypto from 'expo-crypto'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist'; @@ -28,6 +29,12 @@ interface Props { route: RouteProp; } +// This selector takes the state as input and returns a list of sorted player IDs. +const selectSortedPlayerIds = createSelector( + selectSortedPlayers, + (players) => players.map(player => player.id) +); + const SettingsScreen: React.FunctionComponent = ({ navigation }) => { const dispatch = useAppDispatch(); @@ -35,7 +42,7 @@ const SettingsScreen: React.FunctionComponent = ({ navigation }) => { if (typeof currentGameId == 'undefined') return null; const currentGame = useAppSelector(selectCurrentGame); - const players = useAppSelector(selectSortedPlayers); + const playerIds = useAppSelector(selectSortedPlayerIds); const [edit, setEdit] = React.useState(false); @@ -46,7 +53,7 @@ const SettingsScreen: React.FunctionComponent = ({ navigation }) => { dispatch(playerAdd({ id: newPlayerId, - playerName: `Player ${players.length + 1}`, + playerName: `Player ${playerIds.length + 1}`, scores: [0], })); @@ -59,25 +66,25 @@ const SettingsScreen: React.FunctionComponent = ({ navigation }) => { await analytics().logEvent('add_player', { game_id: currentGameId, - player_count: players.length + 1, + player_count: playerIds.length + 1, }); }; useEffect(() => { - if (players.length <= 1) { + if (playerIds.length <= 1) { setEdit(false); } - }, [players]); + }, [playerIds.length]); const ListFooter = () => ( - {players.length < MAX_PLAYERS && + {playerIds.length < MAX_PLAYERS &&