Compare commits
No commits in common. "main" and "vutukuri15/7" have entirely different histories.
main
...
vutukuri15
@ -1,4 +1,3 @@
|
|||||||
EXPO_PUBLIC_API_URL=https://api.openai.com/v1/chat/completions
|
API_KEY=Put Open AI key here
|
||||||
EXPO_PUBLIC_API_KEY=put-open-ai-key-here
|
GPT_MODEL=gpt-4o-mini
|
||||||
EXPO_PUBLIC_MODEL_NAME=gpt-4o-mini
|
#GPT_MODEL=gpt-4-turbo # More expensive model, use sparingly
|
||||||
#EXPO_PUBLIC_MODEL_NAME=gpt-4-turbo # More expensive model, use sparingly
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,6 +37,4 @@ yarn-error.*
|
|||||||
|
|
||||||
app-example
|
app-example
|
||||||
android
|
android
|
||||||
ios
|
|
||||||
.env
|
.env
|
||||||
coverage
|
|
22
README.md
22
README.md
@ -18,15 +18,14 @@ This is an [Expo](https://expo.dev) project created with [`create-expo-app`](htt
|
|||||||
|
|
||||||
To set up your environment variables:
|
To set up your environment variables:
|
||||||
|
|
||||||
1. Copy the example environment variable file to create your own `.env` file:
|
1. Copy the example environment variable file to create your own .env file:
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
|
||||||
|
|
||||||
2. Open the `.env` file and add your OpenAI API key:
|
2. Open the .env file and add your OpenAI API key:
|
||||||
|
|
||||||
`EXPO_PUBLIC_API_KEY=put-open-ai-key-here`
|
API_KEY=your_openai_api_key_here
|
||||||
|
MODEL_NAME=your_model_name
|
||||||
|
|
||||||
3. Save the .env file.
|
3. Save the .env file.
|
||||||
|
|
||||||
@ -60,19 +59,6 @@ In the output, you'll find options to open the app in a
|
|||||||
|
|
||||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||||
|
|
||||||
### Android APK build
|
|
||||||
|
|
||||||
To create an APK build, use the following
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx expo prebuild
|
|
||||||
cd android
|
|
||||||
./gradlew assembleRelease # linux
|
|
||||||
./gradlew.bat assembleRelease # windows command
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, see `android/app/build/outputs/apk/release/app-release.apk` for the output
|
|
||||||
|
|
||||||
### Learn more
|
### Learn more
|
||||||
|
|
||||||
To learn more about developing your project with Expo, look at the following resources:
|
To learn more about developing your project with Expo, look at the following resources:
|
||||||
|
10
app.json
10
app.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Poker Chips Helper",
|
"name": "poker-chips-helper",
|
||||||
"slug": "poker-chips-helper",
|
"slug": "poker-chips-helper",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
@ -9,12 +9,11 @@
|
|||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true
|
||||||
"bundleIdentifier": "com.anonymous.pokerchipshelper"
|
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/icon.png",
|
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"package": "com.anonymous.pokerchipshelper"
|
"package": "com.anonymous.pokerchipshelper"
|
||||||
@ -34,8 +33,7 @@
|
|||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"expo-localization"
|
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
@ -1,59 +1,5 @@
|
|||||||
import i18n from "@/i18n/i18n";
|
|
||||||
import { COLORS } from "@/styles/styles";
|
|
||||||
import AppContext, { IAppContext } from "@/util/context";
|
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import React, { useMemo, useState } from "react";
|
|
||||||
import { I18nextProvider, useTranslation } from "react-i18next";
|
|
||||||
import { useColorScheme } from "react-native";
|
|
||||||
|
|
||||||
const RootLayout: React.FC = () => {
|
export default function RootLayout() {
|
||||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
return <Stack />;
|
||||||
|
}
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const ctx = useMemo<IAppContext>(
|
|
||||||
() => ({
|
|
||||||
showSettings,
|
|
||||||
}),
|
|
||||||
[showSettings]
|
|
||||||
);
|
|
||||||
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<AppContext.Provider value={ctx}>
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<Stack
|
|
||||||
screenOptions={{
|
|
||||||
contentStyle: {
|
|
||||||
backgroundColor: colors.BACKGROUND,
|
|
||||||
},
|
|
||||||
headerShown: true,
|
|
||||||
title: t("poker_chips_helper"),
|
|
||||||
navigationBarColor: colors.PRIMARY,
|
|
||||||
headerRight: () => (
|
|
||||||
<MaterialIcons
|
|
||||||
name="settings"
|
|
||||||
onPress={() => setShowSettings(!showSettings)}
|
|
||||||
size={30}
|
|
||||||
color={colors.TEXT}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
headerStyle: {
|
|
||||||
backgroundColor: colors.PRIMARY,
|
|
||||||
},
|
|
||||||
headerTintColor: colors.TEXT,
|
|
||||||
statusBarBackgroundColor: "grey",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</I18nextProvider>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootLayout;
|
|
||||||
|
259
app/index.tsx
259
app/index.tsx
@ -1,237 +1,64 @@
|
|||||||
import React, { useState, useEffect, useContext, useMemo } from "react";
|
import React, { useState } from "react";
|
||||||
import { ScrollView, Alert, useColorScheme, Appearance } from "react-native";
|
import { ScrollView, Text, Alert, Button } from "react-native";
|
||||||
import Button from "@/containers/Button";
|
|
||||||
import PlayerSelector from "@/components/PlayerSelector";
|
import PlayerSelector from "@/components/PlayerSelector";
|
||||||
import BuyInSelector from "@/components/BuyInSelector";
|
import BuyInSelector from "@/components/BuyInSelector";
|
||||||
import ChipsSelector from "@/components/ChipsSelector";
|
import ChipsSelector from "@/components/ChipsSelector";
|
||||||
import ChipDistributionSummary from "@/components/ChipDistributionSummary";
|
import ChipDistributionSummary from "@/components/ChipDistributionSummary";
|
||||||
import ChipDetection from "@/components/ChipDetection";
|
import ChipDetection from "@/components/ChipDetection";
|
||||||
import CurrencySelector from "@/components/CurrencySelector";
|
|
||||||
import { saveState, loadState } from "@/util/CalculatorState";
|
|
||||||
import {
|
|
||||||
savePersistentState,
|
|
||||||
loadPersistentState,
|
|
||||||
} from "@/util/PersistentState";
|
|
||||||
import styles, { COLORS } from "@/styles/styles";
|
|
||||||
import Section from "@/containers/Section";
|
|
||||||
import AppContext from "@/util/context";
|
|
||||||
import i18n from "@/i18n/i18n";
|
|
||||||
import { Picker, PickerItem } from "@/containers/Picker";
|
|
||||||
import { ItemValue } from "@react-native-picker/picker/typings/Picker";
|
|
||||||
|
|
||||||
const IndexScreen: React.FC = () => {
|
const IndexScreen = () => {
|
||||||
const [playerCount, setPlayerCount] = useState(2);
|
const [playerCount, setPlayerCount] = useState(2);
|
||||||
const [buyInAmount, setBuyInAmount] = useState<number>(20);
|
const [buyInAmount, setBuyInAmount] = useState<number | null>(null);
|
||||||
const [numberOfChips, setNumberOfChips] = useState<number>(5);
|
const [numberOfChips, setNumberOfChips] = useState<number>(5);
|
||||||
const [totalChipsCount, setTotalChipsCount] = useState<number[]>([]);
|
const [totalChipsCount, setTotalChipsCount] = useState<number[]>([]);
|
||||||
const [selectedCurrency, setSelectedCurrency] = useState<string>("$");
|
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState<string>("en");
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
const context = useContext(AppContext);
|
|
||||||
const isSettingsVisible = useMemo(() => context.showSettings, [context]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSave = () => {
|
||||||
const loadPersistentData = async () => {
|
|
||||||
try {
|
|
||||||
const savedState = await loadPersistentState();
|
|
||||||
if (savedState) {
|
|
||||||
setPlayerCount(savedState.playerCount || 2);
|
|
||||||
setBuyInAmount(savedState.buyInAmount || 20);
|
|
||||||
setNumberOfChips(savedState.numberOfChips || 5);
|
|
||||||
setTotalChipsCount(savedState.totalChipsCount || []);
|
|
||||||
setSelectedCurrency(savedState.selectedCurrency || "$");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading persistent state:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadPersistentData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSave = async (slot: "SLOT1" | "SLOT2") => {
|
|
||||||
if (buyInAmount === null) {
|
if (buyInAmount === null) {
|
||||||
Alert.alert(i18n.t("error"), i18n.t("please_select_valid_buyin"));
|
Alert.alert("Error", "Please select a valid buy-in amount");
|
||||||
return;
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
playerCount,
|
|
||||||
buyInAmount,
|
|
||||||
numberOfChips,
|
|
||||||
totalChipsCount,
|
|
||||||
selectedCurrency,
|
|
||||||
};
|
|
||||||
await saveState(slot, state);
|
|
||||||
await savePersistentState(state);
|
|
||||||
Alert.alert(i18n.t("success"), i18n.t("state_saved", { slot }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoad = async (slot: "SLOT1" | "SLOT2") => {
|
|
||||||
const loadedState = await loadState(slot);
|
|
||||||
if (loadedState) {
|
|
||||||
setPlayerCount(loadedState.playerCount);
|
|
||||||
setBuyInAmount(loadedState.buyInAmount ?? 20);
|
|
||||||
setNumberOfChips(loadedState.numberOfChips);
|
|
||||||
setTotalChipsCount(loadedState.totalChipsCount);
|
|
||||||
setSelectedCurrency(loadedState.selectedCurrency || "$");
|
|
||||||
await savePersistentState(loadedState);
|
|
||||||
Alert.alert(i18n.t("success"), i18n.t("state_loaded_from", { slot }));
|
|
||||||
} else {
|
} else {
|
||||||
Alert.alert(i18n.t("info"), i18n.t("no_saved_state_found"));
|
Alert.alert(
|
||||||
|
"Success",
|
||||||
|
`Buy-in amount set to ${buyInAmount} for ${playerCount} players`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLanguageChange = (language: ItemValue, _: any) => {
|
// Update chip count based on detection or manual edit
|
||||||
setSelectedLanguage(language.toString());
|
const updateChipCount = (chipData: { [color: string]: number }) => {
|
||||||
i18n.changeLanguage(language.toString());
|
// Convert the chip data from the API response or manual edit to a count array
|
||||||
|
const chipCountArray = Object.entries(chipData).map(
|
||||||
|
([color, count]) => count
|
||||||
|
);
|
||||||
|
setTotalChipsCount(chipCountArray); // Update the parent component's state
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView contentContainerStyle={{ padding: 20, flexGrow: 1 }}>
|
||||||
style={styles.scrollView}
|
<Text style={{ fontSize: 24, marginBottom: 30, marginTop: 50 }}>
|
||||||
contentContainerStyle={styles.scrollViewContent}
|
Poker Chip Helper
|
||||||
>
|
</Text>
|
||||||
{isSettingsVisible && (
|
<PlayerSelector
|
||||||
<>
|
playerCount={playerCount}
|
||||||
<Section
|
setPlayerCount={setPlayerCount}
|
||||||
title={i18n.t("appearance")}
|
/>
|
||||||
iconName={"brightness-4"}
|
<BuyInSelector setBuyInAmount={setBuyInAmount} />
|
||||||
orientation="row"
|
<ChipDetection updateChipCount={updateChipCount} />
|
||||||
>
|
<ChipsSelector
|
||||||
<Button
|
totalChipsCount={totalChipsCount}
|
||||||
title={
|
setTotalChipsCount={setTotalChipsCount}
|
||||||
darkMode
|
numberOfChips={numberOfChips}
|
||||||
? i18n.t("switch_to_light_mode")
|
setNumberOfChips={setNumberOfChips}
|
||||||
: i18n.t("switch_to_dark_mode")
|
/>
|
||||||
}
|
<ChipDistributionSummary
|
||||||
onPress={() =>
|
playerCount={playerCount}
|
||||||
Appearance.setColorScheme(darkMode ? "light" : "dark")
|
buyInAmount={buyInAmount}
|
||||||
}
|
totalChipsCount={totalChipsCount}
|
||||||
/>
|
/>
|
||||||
</Section>
|
<Button
|
||||||
|
title="Save"
|
||||||
<Section
|
onPress={handleSave}
|
||||||
title={i18n.t("select_language")}
|
disabled={buyInAmount === null}
|
||||||
iconName={"language"}
|
/>
|
||||||
orientation="row"
|
|
||||||
>
|
|
||||||
<Picker
|
|
||||||
selectedValue={selectedLanguage}
|
|
||||||
onValueChange={handleLanguageChange}
|
|
||||||
>
|
|
||||||
<PickerItem label={i18n.t("english")} value="en" />
|
|
||||||
<PickerItem label={i18n.t("spanish")} value="es" />
|
|
||||||
</Picker>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title={i18n.t("select_currency")}
|
|
||||||
iconName={"attach-money"}
|
|
||||||
orientation="row"
|
|
||||||
>
|
|
||||||
<CurrencySelector
|
|
||||||
selectedCurrency={selectedCurrency}
|
|
||||||
setSelectedCurrency={setSelectedCurrency}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title={i18n.t("select_number_of_players")}
|
|
||||||
iconName={"people"}
|
|
||||||
orientation="row"
|
|
||||||
contentStyle={{ justifyContent: "center", gap: 30 }}
|
|
||||||
>
|
|
||||||
<PlayerSelector
|
|
||||||
playerCount={playerCount}
|
|
||||||
setPlayerCount={setPlayerCount}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title={i18n.t("select_buyin_amount")}
|
|
||||||
iconName={"monetization-on"}
|
|
||||||
>
|
|
||||||
<BuyInSelector
|
|
||||||
selectedCurrency={selectedCurrency}
|
|
||||||
setBuyInAmount={setBuyInAmount}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title={i18n.t("automatic_chip_detection")}
|
|
||||||
iconName={"camera-alt"}
|
|
||||||
>
|
|
||||||
<ChipDetection
|
|
||||||
updateChipCount={(chipData) => {
|
|
||||||
const chipCountArray = Object.values(chipData);
|
|
||||||
setTotalChipsCount(chipCountArray);
|
|
||||||
setNumberOfChips(chipCountArray.length);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title={i18n.t("manual_chip_adjustment")}
|
|
||||||
iconName={"account-balance"}
|
|
||||||
orientation="row"
|
|
||||||
contentStyle={{ alignItems: "center" }}
|
|
||||||
>
|
|
||||||
<ChipsSelector
|
|
||||||
totalChipsCount={totalChipsCount}
|
|
||||||
setTotalChipsCount={setTotalChipsCount}
|
|
||||||
numberOfChips={numberOfChips}
|
|
||||||
setNumberOfChips={setNumberOfChips}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title={i18n.t("distribution_and_denomination")}
|
|
||||||
iconName={"currency-exchange"}
|
|
||||||
>
|
|
||||||
<ChipDistributionSummary
|
|
||||||
playerCount={playerCount}
|
|
||||||
buyInAmount={buyInAmount}
|
|
||||||
totalChipsCount={totalChipsCount}
|
|
||||||
selectedCurrency={selectedCurrency}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title={i18n.t("save_and_load")}
|
|
||||||
iconName={"save"}
|
|
||||||
orientation="row"
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
title={i18n.t("save_slot_1")}
|
|
||||||
onPress={() => handleSave("SLOT1")}
|
|
||||||
disabled={buyInAmount === null}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
title={i18n.t("save_slot_2")}
|
|
||||||
onPress={() => handleSave("SLOT2")}
|
|
||||||
disabled={buyInAmount === null}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
title={i18n.t("load_slot_1")}
|
|
||||||
onPress={() => handleLoad("SLOT1")}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
title={i18n.t("load_slot_2")}
|
|
||||||
onPress={() => handleLoad("SLOT2")}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</Section>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 22 KiB |
15
babel.config.js
Normal file
15
babel.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: ["babel-preset-expo", "@babel/preset-typescript"],
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
"module:react-native-dotenv",
|
||||||
|
{
|
||||||
|
moduleName: "@env",
|
||||||
|
path: ".env",
|
||||||
|
safe: true,
|
||||||
|
allowUndefined: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"react-native-reanimated/plugin",
|
||||||
|
],
|
||||||
|
};
|
@ -1,47 +1,33 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { View, Text, TextInput, useColorScheme } from "react-native";
|
import {
|
||||||
import styles, { COLORS } from "@/styles/styles";
|
View,
|
||||||
import Button from "@/containers/Button";
|
Text,
|
||||||
import i18n from "@/i18n/i18n";
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
} from "react-native";
|
||||||
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
|
||||||
interface BuyInSelectorProps {
|
interface BuyInSelectorProps {
|
||||||
setBuyInAmount: React.Dispatch<React.SetStateAction<number>>;
|
setBuyInAmount: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
selectedCurrency: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBuyInOptions = [10, 25, 50];
|
const defaultBuyInOptions = [10, 25, 50];
|
||||||
const MIN = 1;
|
|
||||||
const MAX = 200;
|
|
||||||
|
|
||||||
const parseRoundClamp = (num: string): number => {
|
const BuyInSelector: React.FC<BuyInSelectorProps> = ({ setBuyInAmount }) => {
|
||||||
const parsed = parseFloat(num);
|
|
||||||
const rounded = Math.round(parsed);
|
|
||||||
return Math.min(Math.max(rounded, MIN), MAX);
|
|
||||||
};
|
|
||||||
|
|
||||||
const BuyInSelector: React.FC<BuyInSelectorProps> = ({
|
|
||||||
setBuyInAmount,
|
|
||||||
selectedCurrency,
|
|
||||||
}) => {
|
|
||||||
const [customAmount, setCustomAmount] = useState("");
|
const [customAmount, setCustomAmount] = useState("");
|
||||||
const [buyInAmount, setBuyInAmountState] = useState<number | null>(null);
|
const [buyInAmount, setBuyInAmountState] = useState<number | null>(null);
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCustomAmountChange = (value: string) => {
|
const handleCustomAmountChange = (value: string) => {
|
||||||
const numericValue = parseRoundClamp(value);
|
const numericValue = parseFloat(value);
|
||||||
if (!isNaN(numericValue) && numericValue >= 0) {
|
if (!isNaN(numericValue) && numericValue >= 0) {
|
||||||
setCustomAmount(numericValue.toString());
|
setCustomAmount(value);
|
||||||
setBuyInAmountState(numericValue);
|
setBuyInAmountState(numericValue);
|
||||||
setBuyInAmount(numericValue);
|
setBuyInAmount(numericValue);
|
||||||
} else {
|
} else {
|
||||||
setCustomAmount("");
|
setCustomAmount("");
|
||||||
setBuyInAmountState(25);
|
setBuyInAmountState(null);
|
||||||
setBuyInAmount(25);
|
setBuyInAmount(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,35 +38,88 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View style={styles.container}>
|
||||||
<View style={{ ...styles.container, flexDirection: "row" }}>
|
<View style={styles.header}>
|
||||||
|
<MaterialIcons name="monetization-on" size={30} color="green" />
|
||||||
|
<Text style={styles.title}>Select Buy-in Amount:</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.optionsContainer}>
|
||||||
{defaultBuyInOptions.map((amount) => (
|
{defaultBuyInOptions.map((amount) => (
|
||||||
<Button
|
<TouchableOpacity
|
||||||
key={amount}
|
key={amount}
|
||||||
|
style={[
|
||||||
|
styles.buyInButton,
|
||||||
|
buyInAmount === amount ? styles.selectedButton : null,
|
||||||
|
]}
|
||||||
onPress={() => handleBuyInSelection(amount)}
|
onPress={() => handleBuyInSelection(amount)}
|
||||||
title={`${selectedCurrency} ${amount}`}
|
>
|
||||||
/>
|
<Text style={styles.buttonText}>{amount}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.orText}>Or enter a custom amount:</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, { color: colors.TEXT }]}
|
style={styles.input}
|
||||||
placeholderTextColor={colors.TEXT}
|
|
||||||
value={customAmount}
|
value={customAmount}
|
||||||
maxLength={3}
|
|
||||||
onChangeText={handleCustomAmountChange}
|
onChangeText={handleCustomAmountChange}
|
||||||
placeholder={`${i18n.t("custom_buy_in")} ${MIN} - ${MAX}`}
|
placeholder="Enter custom buy-in"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={[styles.h2, { color: colors.TEXT }]}>
|
<Text style={styles.selectionText}>
|
||||||
{`${i18n.t("selected_buy_in")} `}
|
Selected Buy-in: {buyInAmount !== null ? buyInAmount : "None"}
|
||||||
{buyInAmount !== null
|
|
||||||
? `${selectedCurrency} ${buyInAmount}`
|
|
||||||
: i18n.t("none")}
|
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
optionsContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-around",
|
||||||
|
marginVertical: 10,
|
||||||
|
},
|
||||||
|
buyInButton: {
|
||||||
|
backgroundColor: "#ddd",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
selectedButton: {
|
||||||
|
backgroundColor: "#4caf50",
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
orText: {
|
||||||
|
marginTop: 10,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ccc",
|
||||||
|
padding: 8,
|
||||||
|
marginVertical: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
selectionText: {
|
||||||
|
marginTop: 15,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default BuyInSelector;
|
export default BuyInSelector;
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Image, ActivityIndicator, Text, View } from "react-native";
|
import {
|
||||||
import Button from "@/containers/Button";
|
View,
|
||||||
|
Button,
|
||||||
|
Image,
|
||||||
|
ActivityIndicator,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
} from "react-native";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import i18n from "@/i18n/i18n";
|
import { API_KEY, MODEL_NAME } from "@env";
|
||||||
|
|
||||||
const ChipDetection = ({
|
const ChipDetection = ({ updateChipCount }) => {
|
||||||
updateChipCount,
|
const [imageUri, setImageUri] = useState(null);
|
||||||
}: {
|
|
||||||
updateChipCount: (chipData: Record<string, number>) => void;
|
|
||||||
}) => {
|
|
||||||
const [imageUri, setImageUri] = useState<string | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState(null);
|
||||||
const [lastDetectedChips, setLastDetectedChips] = useState<
|
const [lastDetectedChips, setLastDetectedChips] = useState({});
|
||||||
Record<string, number>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const chipColors = ["white", "red", "green", "blue", "black"];
|
|
||||||
|
|
||||||
const requestCameraPermissions = async () => {
|
const requestCameraPermissions = async () => {
|
||||||
const cameraPermission = await ImagePicker.requestCameraPermissionsAsync();
|
const cameraPermission = await ImagePicker.requestCameraPermissionsAsync();
|
||||||
@ -30,16 +28,16 @@ const ChipDetection = ({
|
|||||||
quality: 1,
|
quality: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.assets && result.assets.length > 0) {
|
if (!result.canceled) {
|
||||||
setImageUri(result.assets[0].uri);
|
setImageUri(result.assets[0].uri);
|
||||||
await processImage(result.assets[0].base64 as string);
|
await processImage(result.assets[0].base64);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const takePhoto = async () => {
|
const takePhoto = async () => {
|
||||||
const hasPermission = await requestCameraPermissions();
|
const hasPermission = await requestCameraPermissions();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
setError(i18n.t("camera_permission_required"));
|
setError("Camera permission is required to take a photo.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,106 +46,90 @@ const ChipDetection = ({
|
|||||||
quality: 1,
|
quality: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.assets && result.assets.length > 0) {
|
if (!result.canceled) {
|
||||||
setImageUri(result.assets[0].uri);
|
setImageUri(result.assets[0].uri);
|
||||||
await processImage(result.assets[0].base64 as string);
|
await processImage(result.assets[0].base64);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processImage = async (base64Image: string) => {
|
const processImage = async (base64Image) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(process.env.EXPO_PUBLIC_API_URL as string, {
|
const response = await fetch(
|
||||||
method: "POST",
|
"https://api.openai.com/v1/chat/completions",
|
||||||
headers: {
|
{
|
||||||
Authorization: `Bearer ${process.env.EXPO_PUBLIC_API_KEY}`,
|
method: "POST",
|
||||||
"Content-Type": "application/json",
|
headers: {
|
||||||
},
|
Authorization: `Bearer ${API_KEY}`,
|
||||||
body: JSON.stringify({
|
"Content-Type": "application/json",
|
||||||
model: process.env.EXPO_PUBLIC_MODEL_NAME,
|
},
|
||||||
messages: [
|
body: JSON.stringify({
|
||||||
{
|
model: MODEL_NAME,
|
||||||
role: "system",
|
messages: [
|
||||||
content:
|
{
|
||||||
"Identify and count poker chips by color. Return only the count for each color in JSON format.",
|
role: "system",
|
||||||
},
|
content:
|
||||||
{
|
"Identify and count poker chips by color. Return only the count for each color in JSON format.",
|
||||||
role: "user",
|
},
|
||||||
content: [
|
{
|
||||||
{
|
role: "user",
|
||||||
type: "text",
|
content: [
|
||||||
text: "How many poker chips are there for each color? Return structured JSON.",
|
{
|
||||||
},
|
type: "text",
|
||||||
{
|
text: "How many poker chips are there for each color? Return structured JSON.",
|
||||||
type: "image_url",
|
},
|
||||||
image_url: { url: `data:image/png;base64,${base64Image}` },
|
{
|
||||||
},
|
type: "image_url",
|
||||||
],
|
image_url: { url: `data:image/png;base64,${base64Image}` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
max_tokens: 1000,
|
},
|
||||||
}),
|
],
|
||||||
});
|
max_tokens: 1000,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok || !result.choices || !result.choices[0].message) {
|
if (!response.ok || !result.choices || !result.choices[0].message) {
|
||||||
throw new Error(i18n.t("invalid_response"));
|
throw new Error("Invalid response from API.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawContent = result.choices[0].message.content.trim();
|
const rawContent = result.choices[0].message.content.trim();
|
||||||
const cleanJSON = rawContent.replace(/```json|```/g, "").trim();
|
const cleanJSON = rawContent.replace(/```json|```/g, "").trim();
|
||||||
const parsedData: Record<string, number> = JSON.parse(cleanJSON);
|
|
||||||
|
|
||||||
const filteredData = Object.entries(parsedData)
|
const parsedData = JSON.parse(cleanJSON);
|
||||||
.filter(([color]) => chipColors.includes(color))
|
|
||||||
.sort(
|
|
||||||
([colorA], [colorB]) =>
|
|
||||||
chipColors.indexOf(colorA) - chipColors.indexOf(colorB)
|
|
||||||
)
|
|
||||||
.reduce(
|
|
||||||
(acc, [color, count]) => {
|
|
||||||
acc[color] = count;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, number>
|
|
||||||
);
|
|
||||||
|
|
||||||
setLastDetectedChips(filteredData);
|
// Filter out colors with a count of 0
|
||||||
|
const filteredData = Object.fromEntries(
|
||||||
|
Object.entries(parsedData).filter(([_, count]) => count > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
setLastDetectedChips(filteredData); // Store detected chip counts
|
||||||
updateChipCount(filteredData);
|
updateChipCount(filteredData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(i18n.t("failed_to_analyze_image"));
|
setError("Failed to analyze the image.");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<ScrollView contentContainerStyle={{ padding: 20, alignItems: "center" }}>
|
||||||
<View
|
<Button title="Pick an Image" onPress={pickImage} />
|
||||||
style={{
|
<Button title="Take a Photo" onPress={takePhoto} />
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-evenly",
|
|
||||||
marginBottom: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button title={i18n.t("pick_an_image")} onPress={pickImage} />
|
|
||||||
<Button title={i18n.t("take_a_photo")} onPress={takePhoto} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{imageUri && (
|
{imageUri && (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: imageUri }}
|
source={{ uri: imageUri }}
|
||||||
style={{ width: 300, height: 300, marginTop: 10 }}
|
style={{ width: 300, height: 300, marginTop: 10 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && <ActivityIndicator size="large" color="blue" />}
|
{loading && <ActivityIndicator size="large" color="blue" />}
|
||||||
|
|
||||||
{error && <Text style={{ color: "red", marginTop: 10 }}>{error}</Text>}
|
{error && <Text style={{ color: "red", marginTop: 10 }}>{error}</Text>}
|
||||||
</View>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,200 +1,89 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { View, Text, Alert } from "react-native";
|
import { View, Text, StyleSheet } from "react-native";
|
||||||
import { ColorValue } from "react-native";
|
import { ColorValue } from "react-native";
|
||||||
import i18n from "@/i18n/i18n";
|
|
||||||
import styles, { COLORS } from "@/styles/styles";
|
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
|
||||||
|
|
||||||
interface ChipDistributionSummaryProps {
|
interface ChipDistributionSummaryProps {
|
||||||
playerCount: number;
|
playerCount: number;
|
||||||
buyInAmount: number;
|
buyInAmount: number | null;
|
||||||
totalChipsCount: number[];
|
totalChipsCount: number[];
|
||||||
colors?: ColorValue[];
|
colors?: ColorValue[];
|
||||||
selectedCurrency: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reverseFib: number[] = [8, 5, 3, 2, 1];
|
const MAX_CHIPS = 500;
|
||||||
|
|
||||||
const ChipDistributionSummary = ({
|
const ChipDistributionSummary = ({
|
||||||
playerCount,
|
playerCount,
|
||||||
buyInAmount,
|
buyInAmount,
|
||||||
totalChipsCount,
|
totalChipsCount,
|
||||||
colors = ["white", "red", "green", "blue", "black"],
|
colors = ["white", "red", "green", "blue", "black"],
|
||||||
selectedCurrency = "$",
|
|
||||||
}: ChipDistributionSummaryProps) => {
|
}: ChipDistributionSummaryProps) => {
|
||||||
const validDenominations: validDenomination[] = [
|
const [chipDistribution, setChipDistribution] = useState<number[]>([]);
|
||||||
0.05, 0.1, 0.25, 1, 5, 10, 20, 50, 100,
|
|
||||||
];
|
|
||||||
const [denominations, setDenominations] = useState<validDenomination[]>([]);
|
|
||||||
const [distributions, setDistributions] = useState<number[]>([]);
|
|
||||||
|
|
||||||
const showAlert = () => {
|
|
||||||
Alert.alert(i18n.t("warning"), i18n.t("chip_value_warn"));
|
|
||||||
};
|
|
||||||
|
|
||||||
type validDenomination =
|
|
||||||
| 0.05
|
|
||||||
| 0.1
|
|
||||||
| 0.25
|
|
||||||
| 0.5
|
|
||||||
| 1
|
|
||||||
| 2
|
|
||||||
| 2.5
|
|
||||||
| 5
|
|
||||||
| 10
|
|
||||||
| 20
|
|
||||||
| 25
|
|
||||||
| 50
|
|
||||||
| 100;
|
|
||||||
|
|
||||||
const findFloorDenomination = (target: number): validDenomination => {
|
|
||||||
let current: validDenomination = validDenominations[0];
|
|
||||||
validDenominations.forEach((value, _) => {
|
|
||||||
if (value < target) current = value;
|
|
||||||
});
|
|
||||||
return current;
|
|
||||||
};
|
|
||||||
|
|
||||||
const round = useCallback((num: number) => Math.round(num * 100) / 100, []);
|
|
||||||
|
|
||||||
// Bound for the value of the highest chip
|
|
||||||
// This is somewhat arbitray and imperfect, but 1/3 to 1/5 is reasonable depending on the number of colors.
|
|
||||||
// Could be possibly improved based on value of buy in amount
|
|
||||||
const maxDenomination: validDenomination = useMemo(() => {
|
|
||||||
let max: validDenomination;
|
|
||||||
switch (totalChipsCount.length) {
|
|
||||||
case 5:
|
|
||||||
case 4:
|
|
||||||
max = findFloorDenomination(buyInAmount / 3);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
max = findFloorDenomination(buyInAmount / 4);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
case 1:
|
|
||||||
default:
|
|
||||||
max = findFloorDenomination(buyInAmount / 5);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return max;
|
|
||||||
}, [totalChipsCount, buyInAmount]);
|
|
||||||
|
|
||||||
const potValue = useMemo(
|
|
||||||
() => buyInAmount * playerCount,
|
|
||||||
[buyInAmount, playerCount]
|
|
||||||
);
|
|
||||||
|
|
||||||
// The total value of all chips distributed to a single player. Ideally should be equal to buyInAmount
|
|
||||||
const totalValue = useMemo(() => {
|
|
||||||
let value = 0;
|
|
||||||
for (let i = 0; i < distributions.length; i++) {
|
|
||||||
value += distributions[i] * denominations[i];
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}, [distributions, denominations]);
|
|
||||||
|
|
||||||
// Maximum quantity of each chip color which may be distributed to a single player before running out
|
|
||||||
const maxPossibleDistribution = useMemo(
|
|
||||||
() => totalChipsCount.map((v) => Math.floor(v / playerCount)),
|
|
||||||
[totalChipsCount, playerCount]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Dynamically set denominations and distributions from changing inputs
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let testDenomination: validDenomination[] = [];
|
if (buyInAmount !== null && playerCount > 0) {
|
||||||
const totalNumColors = totalChipsCount.length;
|
let totalChips = totalChipsCount.reduce((sum, count) => sum + count, 0);
|
||||||
|
|
||||||
// Start with max denominations, then push on the next adjacent lower denomination
|
if (totalChips > MAX_CHIPS) {
|
||||||
testDenomination.push(maxDenomination);
|
const scaleFactor = MAX_CHIPS / totalChips;
|
||||||
let currentDenominationIndex: number =
|
totalChipsCount = totalChipsCount.map((count) =>
|
||||||
validDenominations.indexOf(maxDenomination);
|
Math.floor(count * scaleFactor)
|
||||||
for (
|
);
|
||||||
let i = totalNumColors - 2;
|
totalChips = MAX_CHIPS;
|
||||||
i >= 0 && currentDenominationIndex > 0;
|
|
||||||
i = i - 1
|
|
||||||
) {
|
|
||||||
currentDenominationIndex -= 1;
|
|
||||||
const currentDemoniation = validDenominations[currentDenominationIndex];
|
|
||||||
testDenomination.push(currentDemoniation);
|
|
||||||
}
|
|
||||||
testDenomination.reverse();
|
|
||||||
let numColors = testDenomination.length;
|
|
||||||
|
|
||||||
const testDistribution: number[] = [];
|
|
||||||
for (let i = 0; i < numColors; ++i) {
|
|
||||||
testDistribution.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Distribute the chips using the test denomination with a reverse fibbonaci preference
|
|
||||||
// Not optimal, nor correct under all inputs but works for most inputs
|
|
||||||
// Algorithm could be improved with more complexity and optimization (re-tries, redenominating, etc.)
|
|
||||||
let remainingValue = buyInAmount;
|
|
||||||
let stop = false;
|
|
||||||
while (remainingValue > 0 && !stop) {
|
|
||||||
let distributed = false;
|
|
||||||
for (let i = numColors - 1; i >= 0; i = i - 1) {
|
|
||||||
for (
|
|
||||||
let j = reverseFib[i];
|
|
||||||
j > 0 &&
|
|
||||||
remainingValue >= testDenomination[i] &&
|
|
||||||
testDistribution[i] < maxPossibleDistribution[i];
|
|
||||||
j = j - 1
|
|
||||||
) {
|
|
||||||
testDistribution[i] = testDistribution[i] + 1;
|
|
||||||
remainingValue = round(remainingValue - testDenomination[i]);
|
|
||||||
distributed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (distributed == false) {
|
|
||||||
stop = true;
|
|
||||||
}
|
}
|
||||||
|
const distribution = totalChipsCount.map((chipCount) =>
|
||||||
|
Math.floor(chipCount / playerCount)
|
||||||
|
);
|
||||||
|
|
||||||
|
setChipDistribution(distribution);
|
||||||
|
} else {
|
||||||
|
setChipDistribution([]);
|
||||||
}
|
}
|
||||||
setDenominations(testDenomination);
|
}, [buyInAmount, playerCount, totalChipsCount]);
|
||||||
setDistributions(testDistribution);
|
|
||||||
}, [totalChipsCount, maxDenomination, buyInAmount, playerCount]);
|
const hasValidDistribution = useMemo(
|
||||||
|
() =>
|
||||||
|
buyInAmount !== null && playerCount > 0 && chipDistribution.length > 0,
|
||||||
|
[buyInAmount, playerCount, chipDistribution]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View style={styles.container}>
|
||||||
<View style={styles.container}>
|
<Text style={styles.title}>Chip Distribution Summary:</Text>
|
||||||
{distributions.map((distribution, index) => {
|
{hasValidDistribution ? (
|
||||||
return (
|
chipDistribution.map((count, index) => (
|
||||||
distribution > 0 && (
|
<Text key={index} style={[styles.chipText, { color: colors[index] }]}>
|
||||||
<View style={{ flexDirection: "row" }} key={index}>
|
{`${colors[index]?.toString().toUpperCase()} Chips: ${count} per player`}
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
...styles.h2,
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: colors[index],
|
|
||||||
...(colors[index] === "white" && styles.shadow),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${distribution} ${i18n.t("chips")}: ${selectedCurrency}${denominations[index]} ${i18n.t("each")}`}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
|
|
||||||
<View style={[styles.container, { flexDirection: "row", gap: 1 }]}>
|
|
||||||
<Text style={styles.p}>
|
|
||||||
{i18n.t("total_value")}: {selectedCurrency} {round(totalValue)}{" "}
|
|
||||||
</Text>
|
</Text>
|
||||||
{round(totalValue) !== buyInAmount && (
|
))
|
||||||
<MaterialIcons
|
) : (
|
||||||
name="warning"
|
<Text style={styles.noDataText}>
|
||||||
size={20}
|
No valid distribution calculated yet.
|
||||||
color={COLORS.WARNING}
|
|
||||||
onPress={showAlert}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<Text style={styles.p}>
|
|
||||||
{selectedCurrency} {potValue} {i18n.t("pot")}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
)}
|
||||||
</>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginTop: 20,
|
||||||
|
padding: 15,
|
||||||
|
backgroundColor: "#F8F9FA",
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
chipText: {
|
||||||
|
fontSize: 16,
|
||||||
|
marginVertical: 2,
|
||||||
|
},
|
||||||
|
noDataText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "gray",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default ChipDistributionSummary;
|
export default ChipDistributionSummary;
|
||||||
|
@ -4,17 +4,11 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
Button,
|
||||||
ColorValue,
|
ColorValue,
|
||||||
Modal,
|
Modal,
|
||||||
TouchableOpacity,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import Button from "@/containers/Button";
|
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
|
||||||
import styles from "@/styles/styles";
|
|
||||||
import i18n from "@/i18n/i18n";
|
|
||||||
|
|
||||||
const colors: ColorValue[] = ["white", "red", "green", "blue", "black"];
|
const colors: ColorValue[] = ["white", "red", "green", "blue", "black"];
|
||||||
const defaults = [100, 50, 50, 50, 50];
|
|
||||||
|
|
||||||
const ChipInputModal = ({
|
const ChipInputModal = ({
|
||||||
showModal,
|
showModal,
|
||||||
@ -30,35 +24,23 @@ const ChipInputModal = ({
|
|||||||
const color: ColorValue = useMemo(() => showModal[1], [showModal]);
|
const color: ColorValue = useMemo(() => showModal[1], [showModal]);
|
||||||
const colorIdx = useMemo(() => colors.indexOf(color), [color]);
|
const colorIdx = useMemo(() => colors.indexOf(color), [color]);
|
||||||
|
|
||||||
const [value, setValue] = useState<number | undefined>();
|
const [value, setValue] = useState<number | undefined>(); // value may be undefined initially
|
||||||
|
|
||||||
|
// Reset the color value when the specific color this modal is for, changes. The same modal is shared/reused in all cases.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(totalChipsCount[colorIdx]);
|
setValue(totalChipsCount[colorIdx]);
|
||||||
}, [colorIdx, totalChipsCount]);
|
}, [colorIdx]);
|
||||||
|
|
||||||
const shadow = useMemo(() => color === "white", [color]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={showModal[0]}
|
visible={showModal[0]}
|
||||||
onRequestClose={() => setShowModal([false, color])}
|
onRequestClose={() => setShowModal([false, color])}
|
||||||
style={styles.modal}
|
|
||||||
presentationStyle="fullScreen"
|
|
||||||
animationType="slide"
|
|
||||||
>
|
>
|
||||||
{value !== undefined && (
|
{value !== undefined && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.h2}>
|
<Text>Number of {showModal[1]?.toString()} chips</Text>
|
||||||
{i18n.t("number_of_chips", {
|
|
||||||
color: showModal[1]?.toString(),
|
|
||||||
})}{" "}
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{
|
style={{ color: showModal[1] }}
|
||||||
...styles.input,
|
|
||||||
color: showModal[1],
|
|
||||||
...(shadow ? styles.shadow : {}),
|
|
||||||
}}
|
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={value.toString()}
|
value={value.toString()}
|
||||||
onChangeText={(v) => {
|
onChangeText={(v) => {
|
||||||
@ -70,7 +52,7 @@ const ChipInputModal = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
title={i18n.t("accept")}
|
title="Accept"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
update(showModal[1], Number.isNaN(value) ? 0 : value);
|
update(showModal[1], Number.isNaN(value) ? 0 : value);
|
||||||
setShowModal([false, color]);
|
setShowModal([false, color]);
|
||||||
@ -89,25 +71,14 @@ const Chip = ({
|
|||||||
count: number;
|
count: number;
|
||||||
setShowModal: React.Dispatch<React.SetStateAction<[boolean, ColorValue]>>;
|
setShowModal: React.Dispatch<React.SetStateAction<[boolean, ColorValue]>>;
|
||||||
}) => {
|
}) => {
|
||||||
const shadow = useMemo(() => color === "white", [color]);
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<Text
|
||||||
|
key={color.toString()}
|
||||||
onPress={() => setShowModal([true, color])}
|
onPress={() => setShowModal([true, color])}
|
||||||
style={{ alignItems: "center" }}
|
style={[{ color: color }, styles.chip]}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons
|
{count}
|
||||||
name="poker-chip"
|
</Text>
|
||||||
size={24}
|
|
||||||
color={color}
|
|
||||||
style={shadow ? styles.shadow : {}}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
key={color.toString()}
|
|
||||||
style={[{ color: color }, styles.h2, shadow ? styles.shadow : {}]}
|
|
||||||
>
|
|
||||||
{count}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -126,12 +97,12 @@ const ChipsSelector = ({
|
|||||||
false,
|
false,
|
||||||
colors[0],
|
colors[0],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const colorsUsed = useMemo(
|
const colorsUsed = useMemo(
|
||||||
() => colors.slice(0, numberOfChips),
|
() => colors.filter((v, i) => i < numberOfChips),
|
||||||
[numberOfChips]
|
[numberOfChips]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Callback for ChipInputModal to update the chips in the parents state.
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(color: ColorValue, count: number) => {
|
(color: ColorValue, count: number) => {
|
||||||
const newTotalChipsCount = totalChipsCount.slice();
|
const newTotalChipsCount = totalChipsCount.slice();
|
||||||
@ -139,18 +110,20 @@ const ChipsSelector = ({
|
|||||||
newTotalChipsCount[colorIndex] = count;
|
newTotalChipsCount[colorIndex] = count;
|
||||||
setTotalChipsCount(newTotalChipsCount);
|
setTotalChipsCount(newTotalChipsCount);
|
||||||
},
|
},
|
||||||
[totalChipsCount, setTotalChipsCount]
|
[numberOfChips, totalChipsCount, setTotalChipsCount]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// When the number of chips changes (dec or inc), update the array being careful to add in sensible default values where they belong
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (numberOfChips !== totalChipsCount.length) {
|
if (numberOfChips !== totalChipsCount.length) {
|
||||||
let newTotalChipsCount = totalChipsCount.slice();
|
let newTotalChipsCount = totalChipsCount.slice();
|
||||||
|
|
||||||
if (numberOfChips < totalChipsCount.length) {
|
if (numberOfChips < totalChipsCount.length) {
|
||||||
newTotalChipsCount = newTotalChipsCount.slice(0, numberOfChips);
|
newTotalChipsCount = newTotalChipsCount.slice(0, numberOfChips);
|
||||||
} else if (numberOfChips > totalChipsCount.length) {
|
} else if (numberOfChips > totalChipsCount.length) {
|
||||||
for (let colorIndex = 0; colorIndex < numberOfChips; ++colorIndex) {
|
for (let colorIndex = 0; colorIndex < numberOfChips; ++colorIndex) {
|
||||||
if (colorIndex >= newTotalChipsCount.length) {
|
if (colorIndex >= newTotalChipsCount.length) {
|
||||||
const defaultTotal = defaults[colorIndex];
|
const defaultTotal = 100 - colorIndex * 20;
|
||||||
newTotalChipsCount.push(defaultTotal);
|
newTotalChipsCount.push(defaultTotal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,33 +134,35 @@ const ChipsSelector = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<View style={styles.container}>
|
||||||
title="-"
|
<Text style={styles.title}>Chips you have</Text>
|
||||||
onPress={() => {
|
<View style={styles.chipContainer}>
|
||||||
setNumberOfChips(Math.max(1, numberOfChips - 1));
|
{colorsUsed.map((color) => (
|
||||||
}}
|
|
||||||
disabled={numberOfChips === 1}
|
|
||||||
/>
|
|
||||||
<View style={[styles.container, { flexDirection: "row" }]}>
|
|
||||||
{colorsUsed.map((color) => {
|
|
||||||
const chipCount = totalChipsCount[colors.indexOf(color)] ?? 0;
|
|
||||||
return (
|
|
||||||
<Chip
|
<Chip
|
||||||
key={color.toString()}
|
key={color.toString()}
|
||||||
color={color}
|
color={color}
|
||||||
count={chipCount}
|
count={totalChipsCount[colors.indexOf(color)] ?? 0}
|
||||||
setShowModal={setShowModal}
|
setShowModal={setShowModal}
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
</View>
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
title="-"
|
||||||
|
onPress={() => {
|
||||||
|
setNumberOfChips(Math.max(1, numberOfChips - 1));
|
||||||
|
}}
|
||||||
|
disabled={numberOfChips == 1}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title="+"
|
||||||
|
onPress={() => {
|
||||||
|
setNumberOfChips(Math.min(5, numberOfChips + 1));
|
||||||
|
}}
|
||||||
|
disabled={numberOfChips == 5}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Button
|
|
||||||
title="+"
|
|
||||||
onPress={() => {
|
|
||||||
setNumberOfChips(Math.min(5, numberOfChips + 1));
|
|
||||||
}}
|
|
||||||
disabled={numberOfChips === 5}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChipInputModal
|
<ChipInputModal
|
||||||
showModal={showModal}
|
showModal={showModal}
|
||||||
@ -199,4 +174,34 @@ const ChipsSelector = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 20,
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
margin: "auto",
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
chipContainer: {
|
||||||
|
padding: 20,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-evenly",
|
||||||
|
backgroundColor: "#bbb",
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-evenly",
|
||||||
|
},
|
||||||
|
button: {},
|
||||||
|
});
|
||||||
export default ChipsSelector;
|
export default ChipsSelector;
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import i18n from "@/i18n/i18n";
|
|
||||||
import { Picker, PickerItem } from "@/containers/Picker";
|
|
||||||
|
|
||||||
interface CurrencySelectorProps {
|
|
||||||
selectedCurrency: string;
|
|
||||||
setSelectedCurrency: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CurrencySelector: React.FC<CurrencySelectorProps> = ({
|
|
||||||
selectedCurrency,
|
|
||||||
setSelectedCurrency,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Picker
|
|
||||||
selectedValue={selectedCurrency}
|
|
||||||
onValueChange={(itemValue) => setSelectedCurrency(itemValue.toString())}
|
|
||||||
testID="currency-picker" // ✅ Add testID here
|
|
||||||
>
|
|
||||||
<PickerItem label={i18n.t("usd")} value="$" />
|
|
||||||
<PickerItem label={i18n.t("euro")} value="€" />
|
|
||||||
<PickerItem label={i18n.t("pound")} value="£" />
|
|
||||||
<PickerItem label={i18n.t("inr")} value="₹" />
|
|
||||||
</Picker>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CurrencySelector;
|
|
@ -1,49 +1,64 @@
|
|||||||
import React, { useMemo } from "react";
|
import React from "react";
|
||||||
import { View, Text, useColorScheme } from "react-native";
|
import { View, Text, Button, Image, StyleSheet } from "react-native";
|
||||||
import Button from "@/containers/Button";
|
|
||||||
import styles, { COLORS } from "@/styles/styles";
|
|
||||||
|
|
||||||
interface PlayerSelectorProps {
|
interface PlayerSelectorProps {
|
||||||
playerCount: number;
|
playerCount: number;
|
||||||
setPlayerCount: React.Dispatch<React.SetStateAction<number>>;
|
setPlayerCount: React.Dispatch<React.SetStateAction<number>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN = 2;
|
|
||||||
const MAX = 8;
|
|
||||||
|
|
||||||
const PlayerSelector: React.FC<PlayerSelectorProps> = ({
|
const PlayerSelector: React.FC<PlayerSelectorProps> = ({
|
||||||
playerCount,
|
playerCount,
|
||||||
setPlayerCount,
|
setPlayerCount,
|
||||||
}) => {
|
}) => {
|
||||||
const increasePlayers = () => {
|
const increasePlayers = () => {
|
||||||
if (playerCount < MAX) setPlayerCount(playerCount + 1);
|
if (playerCount < 8) setPlayerCount(playerCount + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decreasePlayers = () => {
|
const decreasePlayers = () => {
|
||||||
if (playerCount > MIN) setPlayerCount(playerCount - 1);
|
if (playerCount > 2) setPlayerCount(playerCount - 1);
|
||||||
};
|
};
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
<View style={styles.container}>
|
||||||
<Button
|
<View style={styles.header}>
|
||||||
title="-"
|
<Image
|
||||||
onPress={decreasePlayers}
|
source={{
|
||||||
disabled={playerCount <= MIN}
|
uri: "https://static.thenounproject.com/png/3890959-200.png",
|
||||||
/>
|
}}
|
||||||
<Text style={[styles.h1, { color: colors.TEXT }]}>{playerCount}</Text>
|
style={styles.icon}
|
||||||
<Button
|
/>
|
||||||
title="+"
|
<Text style={styles.title}>Select Number of Players:</Text>
|
||||||
onPress={increasePlayers}
|
</View>
|
||||||
disabled={playerCount >= MAX}
|
|
||||||
/>
|
<Text style={styles.playerCount}>{playerCount}</Text>
|
||||||
|
<View style={{ flexDirection: "row" }}>
|
||||||
|
<Button title="-" onPress={decreasePlayers} />
|
||||||
|
<Button title="+" onPress={increasePlayers} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
marginLeft: 10, // Spacing between icon and text
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 48, // Increased size
|
||||||
|
height: 48, // Increased size
|
||||||
|
},
|
||||||
|
playerCount: {
|
||||||
|
fontSize: 24,
|
||||||
|
marginVertical: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default PlayerSelector;
|
export default PlayerSelector;
|
||||||
|
@ -2,8 +2,8 @@ import React from "react";
|
|||||||
import { fireEvent, render } from "@testing-library/react-native";
|
import { fireEvent, render } from "@testing-library/react-native";
|
||||||
import BuyInSelector from "@/components/BuyInSelector";
|
import BuyInSelector from "@/components/BuyInSelector";
|
||||||
|
|
||||||
// Mocking vector icons to prevent errors
|
|
||||||
jest.mock("@expo/vector-icons", () => {
|
jest.mock("@expo/vector-icons", () => {
|
||||||
|
const React = require("react");
|
||||||
const { Text } = require("react-native");
|
const { Text } = require("react-native");
|
||||||
return {
|
return {
|
||||||
MaterialIcons: () => <Text>MaterialIcons</Text>,
|
MaterialIcons: () => <Text>MaterialIcons</Text>,
|
||||||
@ -11,144 +11,78 @@ jest.mock("@expo/vector-icons", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("BuyInSelector Component", () => {
|
describe("BuyInSelector Component", () => {
|
||||||
let setBuyInAmount: jest.Mock;
|
|
||||||
|
|
||||||
// Render the component and return query methods
|
|
||||||
const renderComponent = (selectedCurrency = "$") => {
|
|
||||||
const utils = render(
|
|
||||||
<BuyInSelector
|
|
||||||
setBuyInAmount={setBuyInAmount}
|
|
||||||
selectedCurrency={selectedCurrency}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...utils,
|
|
||||||
getByText: utils.getByText,
|
|
||||||
getByPlaceholderText: utils.getByPlaceholderText,
|
|
||||||
queryByText: utils.queryByText,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setBuyInAmount = jest.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the buy-in options and input correctly", () => {
|
it("renders the buy-in options and input correctly", () => {
|
||||||
const { getByText, getByPlaceholderText, queryByText } = renderComponent();
|
const setBuyInAmount = jest.fn();
|
||||||
|
const { getByText, getByPlaceholderText } = render(
|
||||||
|
<BuyInSelector setBuyInAmount={setBuyInAmount} />
|
||||||
|
);
|
||||||
|
|
||||||
expect(getByText("$ 10")).toBeTruthy();
|
expect(getByText("Select Buy-in Amount:")).toBeTruthy();
|
||||||
expect(getByText("$ 25")).toBeTruthy();
|
expect(getByText("10")).toBeTruthy();
|
||||||
expect(getByText("$ 50")).toBeTruthy();
|
expect(getByText("25")).toBeTruthy();
|
||||||
expect(
|
expect(getByText("50")).toBeTruthy();
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200")
|
expect(getByPlaceholderText("Enter custom buy-in")).toBeTruthy();
|
||||||
).toBeTruthy();
|
|
||||||
expect(queryByText(/Selected Buy-in:.*None/i)).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets a predefined buy-in amount correctly", () => {
|
it("sets a predefined buy-in amount correctly", () => {
|
||||||
const { getByText } = renderComponent();
|
const setBuyInAmount = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<BuyInSelector setBuyInAmount={setBuyInAmount} />
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.press(getByText("25"));
|
||||||
|
|
||||||
fireEvent.press(getByText("$ 25"));
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets a custom buy-in amount correctly", () => {
|
it("sets a custom buy-in amount correctly", () => {
|
||||||
const { getByPlaceholderText } = renderComponent();
|
const setBuyInAmount = jest.fn();
|
||||||
|
const { getByPlaceholderText } = render(
|
||||||
fireEvent.changeText(
|
<BuyInSelector setBuyInAmount={setBuyInAmount} />
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
|
||||||
"100"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "100");
|
||||||
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(100);
|
expect(setBuyInAmount).toHaveBeenCalledWith(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("bound and validate custom amount if invalid input is entered", () => {
|
it("resets custom amount if invalid input is entered", () => {
|
||||||
const { getByPlaceholderText } = renderComponent();
|
const setBuyInAmount = jest.fn();
|
||||||
|
const { getByPlaceholderText } = render(
|
||||||
fireEvent.changeText(
|
<BuyInSelector setBuyInAmount={setBuyInAmount} />
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
|
||||||
"-10"
|
|
||||||
);
|
);
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(1); // Min value
|
|
||||||
|
|
||||||
fireEvent.changeText(
|
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "-10");
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
|
||||||
"abc"
|
expect(setBuyInAmount).toHaveBeenCalledWith(null);
|
||||||
);
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears the custom amount when selecting a predefined option", () => {
|
it("clears the custom amount when selecting a predefined option", () => {
|
||||||
const { getByPlaceholderText, getByText } = renderComponent();
|
const setBuyInAmount = jest.fn();
|
||||||
|
const { getByText, getByPlaceholderText } = render(
|
||||||
fireEvent.changeText(
|
<BuyInSelector setBuyInAmount={setBuyInAmount} />
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
|
||||||
"100"
|
|
||||||
);
|
);
|
||||||
fireEvent.press(getByText("$ 50"));
|
|
||||||
|
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "100");
|
||||||
|
|
||||||
|
fireEvent.press(getByText("50"));
|
||||||
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(50);
|
expect(setBuyInAmount).toHaveBeenCalledWith(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles valid and invalid input for custom amount correctly", () => {
|
it("handles valid and invalid input for custom amount correctly", () => {
|
||||||
const { getByPlaceholderText } = renderComponent();
|
const setBuyInAmount = jest.fn();
|
||||||
|
const { getByPlaceholderText } = render(
|
||||||
fireEvent.changeText(
|
<BuyInSelector setBuyInAmount={setBuyInAmount} />
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
|
||||||
"75"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "75");
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(75);
|
expect(setBuyInAmount).toHaveBeenCalledWith(75);
|
||||||
|
|
||||||
fireEvent.changeText(
|
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "-5");
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
expect(setBuyInAmount).toHaveBeenCalledWith(null);
|
||||||
"-5"
|
|
||||||
);
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(1);
|
|
||||||
|
|
||||||
fireEvent.changeText(
|
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "abc");
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
expect(setBuyInAmount).toHaveBeenCalledWith(null);
|
||||||
"abc"
|
|
||||||
);
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("triggers state update every time a buy-in option is clicked, even if it's the same", () => {
|
|
||||||
const { getByText } = renderComponent();
|
|
||||||
|
|
||||||
fireEvent.press(getByText("$ 25"));
|
|
||||||
fireEvent.press(getByText("$ 25"));
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets to default buy-in when custom input is cleared", () => {
|
|
||||||
const { getByPlaceholderText } = renderComponent();
|
|
||||||
const input = getByPlaceholderText("Or, enter a custom amount: 1 - 200");
|
|
||||||
|
|
||||||
fireEvent.changeText(input, "75");
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(75);
|
|
||||||
|
|
||||||
fireEvent.changeText(input, "");
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates state correctly when selecting predefined buy-in after entering a custom amount", () => {
|
|
||||||
const { getByPlaceholderText, getByText } = renderComponent();
|
|
||||||
|
|
||||||
fireEvent.changeText(
|
|
||||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
|
||||||
"200"
|
|
||||||
);
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(200);
|
|
||||||
|
|
||||||
fireEvent.press(getByText("$ 10"));
|
|
||||||
expect(setBuyInAmount).toHaveBeenCalledWith(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays selected currency correctly", () => {
|
|
||||||
const { getByText, queryByText } = renderComponent("€");
|
|
||||||
|
|
||||||
expect(getByText("€ 10")).toBeTruthy();
|
|
||||||
expect(getByText("€ 25")).toBeTruthy();
|
|
||||||
expect(getByText("€ 50")).toBeTruthy();
|
|
||||||
expect(queryByText(/Selected Buy-in:.*None/i)).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,134 +17,11 @@ jest.mock("expo-image-picker", () => ({
|
|||||||
describe("ChipDetection", () => {
|
describe("ChipDetection", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.spyOn(global, "fetch").mockImplementation(() =>
|
global.fetch = jest.fn(() =>
|
||||||
Promise.resolve(
|
Promise.resolve({
|
||||||
new Response(
|
ok: true,
|
||||||
JSON.stringify({
|
json: () =>
|
||||||
choices: [
|
Promise.resolve({
|
||||||
{
|
|
||||||
message: {
|
|
||||||
content: JSON.stringify({
|
|
||||||
red: 5,
|
|
||||||
green: 3,
|
|
||||||
blue: 0,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders correctly", () => {
|
|
||||||
const { getByText } = render(
|
|
||||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByText(/pick an image/i)).toBeTruthy();
|
|
||||||
expect(getByText(/take a photo/i)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("picks an image from the library", async () => {
|
|
||||||
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
|
|
||||||
canceled: false,
|
|
||||||
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getByText } = render(
|
|
||||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
|
||||||
);
|
|
||||||
fireEvent.press(getByText(/pick an image/i));
|
|
||||||
|
|
||||||
await waitFor(() => expect(mockUpdateChipCount).toHaveBeenCalled());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("takes a photo with the camera", async () => {
|
|
||||||
(
|
|
||||||
ImagePicker.requestCameraPermissionsAsync as jest.Mock
|
|
||||||
).mockResolvedValueOnce({
|
|
||||||
granted: true,
|
|
||||||
});
|
|
||||||
(ImagePicker.launchCameraAsync as jest.Mock).mockResolvedValueOnce({
|
|
||||||
canceled: false,
|
|
||||||
assets: [{ uri: "test-camera-uri", base64: "test-camera-base64" }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getByText } = render(
|
|
||||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
|
||||||
);
|
|
||||||
fireEvent.press(getByText(/take a photo/i));
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(mockUpdateChipCount).toHaveBeenCalledWith({
|
|
||||||
red: 5,
|
|
||||||
green: 3,
|
|
||||||
blue: 0,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles camera permission denied", async () => {
|
|
||||||
(
|
|
||||||
ImagePicker.requestCameraPermissionsAsync as jest.Mock
|
|
||||||
).mockResolvedValueOnce({
|
|
||||||
granted: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { getByText } = render(
|
|
||||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
|
||||||
);
|
|
||||||
fireEvent.press(getByText(/take a photo/i));
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(
|
|
||||||
getByText(/camera permission is required to take a photo/i)
|
|
||||||
).toBeTruthy()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays error message on image processing failure", async () => {
|
|
||||||
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
|
|
||||||
canceled: false,
|
|
||||||
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.spyOn(global, "fetch").mockImplementationOnce(() =>
|
|
||||||
Promise.resolve(
|
|
||||||
new Response(JSON.stringify({ choices: [] }), {
|
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { getByText } = render(
|
|
||||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
|
||||||
);
|
|
||||||
fireEvent.press(getByText(/pick an image/i));
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(getByText(/failed to analyze the image/i)).toBeTruthy()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles valid API response correctly", async () => {
|
|
||||||
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
|
|
||||||
canceled: false,
|
|
||||||
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.spyOn(global, "fetch").mockImplementationOnce(() =>
|
|
||||||
Promise.resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
message: {
|
message: {
|
||||||
@ -153,21 +30,120 @@ describe("ChipDetection", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
})
|
||||||
)
|
);
|
||||||
)
|
});
|
||||||
|
|
||||||
|
it("renders correctly", () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||||
|
);
|
||||||
|
expect(getByText("Pick an Image")).toBeTruthy();
|
||||||
|
expect(getByText("Take a Photo")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("picks an image from the library", async () => {
|
||||||
|
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
|
||||||
|
canceled: false,
|
||||||
|
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||||
|
);
|
||||||
|
fireEvent.press(getByText("Pick an Image"));
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockUpdateChipCount).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("takes a photo with the camera", async () => {
|
||||||
|
ImagePicker.requestCameraPermissionsAsync.mockResolvedValueOnce({
|
||||||
|
granted: true,
|
||||||
|
});
|
||||||
|
ImagePicker.launchCameraAsync.mockResolvedValueOnce({
|
||||||
|
canceled: false,
|
||||||
|
assets: [{ uri: "test-camera-uri", base64: "test-camera-base64" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||||
|
);
|
||||||
|
fireEvent.press(getByText("Take a Photo"));
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockUpdateChipCount).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles camera permission denied", async () => {
|
||||||
|
ImagePicker.requestCameraPermissionsAsync.mockResolvedValueOnce({
|
||||||
|
granted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||||
|
);
|
||||||
|
fireEvent.press(getByText("Take a Photo"));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getByText("Camera permission is required to take a photo.")
|
||||||
|
).toBeTruthy()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays error message on image processing failure", async () => {
|
||||||
|
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
|
||||||
|
canceled: false,
|
||||||
|
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
global.fetch.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ choices: [] }),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||||
);
|
);
|
||||||
fireEvent.press(getByText(/pick an image/i));
|
fireEvent.press(getByText("Pick an Image"));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getByText("Failed to analyze the image.")).toBeTruthy()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles valid API response correctly", async () => {
|
||||||
|
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
|
||||||
|
canceled: false,
|
||||||
|
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
global.fetch.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: JSON.stringify({ red: 5, green: 3 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByText } = render(
|
||||||
|
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||||
|
);
|
||||||
|
fireEvent.press(getByText("Pick an Image"));
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(mockUpdateChipCount).toHaveBeenCalledWith({
|
expect(mockUpdateChipCount).toHaveBeenCalledWith({
|
||||||
red: 5,
|
red: 5,
|
||||||
green: 3,
|
green: 3,
|
||||||
blue: 0,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,59 +1,65 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Alert } from "react-native";
|
import { render } from "@testing-library/react-native";
|
||||||
|
|
||||||
import { fireEvent, render } from "@testing-library/react-native";
|
|
||||||
import ChipDistributionSummary from "../ChipDistributionSummary";
|
import ChipDistributionSummary from "../ChipDistributionSummary";
|
||||||
|
|
||||||
jest.mock("@expo/vector-icons", () => {
|
|
||||||
const { Text } = require("react-native");
|
|
||||||
return {
|
|
||||||
MaterialIcons: () => <Text>TestIcon</Text>,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ChipDistributionSummary Component", () => {
|
describe("ChipDistributionSummary Component", () => {
|
||||||
test("renders correctly with valid data", () => {
|
test("renders correctly with valid data", () => {
|
||||||
const playerCount = 4;
|
const playerCount = 4;
|
||||||
const totalChipsCount = [100, 80, 60, 40, 20];
|
const totalChipsCount = [100, 200, 300, 400, 500];
|
||||||
const buyInAmount = 20;
|
const colors = ["WHITE", "RED", "GREEN", "BLUE", "BLACK"];
|
||||||
const expectedDistribution = [16, 12, 8, 6, 2];
|
|
||||||
const expectedDenominations = [0.05, 0.1, 0.25, 1, 5];
|
// Update this to match the actual component's chip distribution logic
|
||||||
|
const expectedDistribution = [8, 16, 25, 33, 41]; // Adjust based on actual component calculations
|
||||||
|
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<ChipDistributionSummary
|
<ChipDistributionSummary
|
||||||
playerCount={playerCount}
|
playerCount={playerCount}
|
||||||
buyInAmount={buyInAmount}
|
buyInAmount={100}
|
||||||
totalChipsCount={totalChipsCount}
|
totalChipsCount={totalChipsCount}
|
||||||
selectedCurrency={"$"}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(getByText("Chip Distribution Summary:")).toBeTruthy();
|
||||||
|
|
||||||
expectedDistribution.forEach((count, index) => {
|
expectedDistribution.forEach((count, index) => {
|
||||||
const regex = new RegExp(
|
expect(getByText(new RegExp(`${colors[index]} Chips: ${count} per player`, "i"))).toBeTruthy();
|
||||||
`^${count}\\s+chips:\\s+\\$${expectedDenominations[index]}\\s+Each$`,
|
|
||||||
"i"
|
|
||||||
);
|
|
||||||
expect(getByText(regex)).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders warning message when needed", async () => {
|
test("renders fallback message when no valid distribution", () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ChipDistributionSummary playerCount={0} buyInAmount={null} totalChipsCount={[]} />
|
||||||
|
);
|
||||||
|
expect(getByText("No valid distribution calculated yet.")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("scales down chips if exceeding MAX_CHIPS", () => {
|
||||||
|
const playerCount = 2;
|
||||||
|
let totalChipsCount = [300, 400, 500, 600, 700];
|
||||||
|
const MAX_CHIPS = 500;
|
||||||
|
const totalChips = totalChipsCount.reduce((sum, count) => sum + count, 0);
|
||||||
|
|
||||||
|
if (totalChips > MAX_CHIPS) {
|
||||||
|
const scaleFactor = MAX_CHIPS / totalChips;
|
||||||
|
totalChipsCount = totalChipsCount.map(count => Math.round(count * scaleFactor));
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedDistribution = [30, 40, 50, 60, 70]; // Adjust to match actual component calculations
|
||||||
|
const colors = ["WHITE", "RED", "GREEN", "BLUE", "BLACK"];
|
||||||
|
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<ChipDistributionSummary
|
<ChipDistributionSummary
|
||||||
playerCount={6}
|
playerCount={playerCount}
|
||||||
buyInAmount={25}
|
buyInAmount={100}
|
||||||
selectedCurrency={"$"}
|
totalChipsCount={totalChipsCount}
|
||||||
totalChipsCount={[100, 50]}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const warning = getByText("TestIcon");
|
|
||||||
expect(warning).toBeTruthy();
|
|
||||||
|
|
||||||
jest.spyOn(Alert, "alert");
|
expect(getByText("Chip Distribution Summary:")).toBeTruthy();
|
||||||
fireEvent.press(warning);
|
|
||||||
expect(Alert.alert).toHaveBeenCalledWith(
|
expectedDistribution.forEach((count, index) => {
|
||||||
"Warning",
|
expect(getByText(new RegExp(`${colors[index]} Chips: ${count} per player`, "i"))).toBeTruthy();
|
||||||
`Be advised that the value of the distributed chips does not equal the buy-in for these inputs.\n\nHowever, results shown are fair to all players`
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,19 +3,13 @@ import {
|
|||||||
userEvent,
|
userEvent,
|
||||||
render,
|
render,
|
||||||
screen,
|
screen,
|
||||||
|
waitForElementToBeRemoved,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
} from "@testing-library/react-native";
|
} from "@testing-library/react-native";
|
||||||
import ChipsSelector from "@/components/ChipsSelector";
|
import ChipsSelector from "@/components/ChipsSelector";
|
||||||
|
|
||||||
const TOTAL_CHIPS_COUNT = [100, 80, 60, 40, 20];
|
const TOTAL_CHIPS_COUNT = [100, 80, 60, 40, 20];
|
||||||
|
|
||||||
jest.mock("@expo/vector-icons", () => {
|
|
||||||
const { Text } = require("react-native");
|
|
||||||
return {
|
|
||||||
MaterialCommunityIcons: () => <Text>TestIcon</Text>,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const mocktTotalChipsCount = jest.fn();
|
const mocktTotalChipsCount = jest.fn();
|
||||||
const mockSetNumberOfChips = jest.fn();
|
const mockSetNumberOfChips = jest.fn();
|
||||||
|
|
||||||
@ -54,8 +48,7 @@ describe("tests for ChipsSelector", () => {
|
|||||||
const green = screen.getByText("60");
|
const green = screen.getByText("60");
|
||||||
expect(green).toHaveStyle({ color: "green" });
|
expect(green).toHaveStyle({ color: "green" });
|
||||||
|
|
||||||
fireEvent.press(green);
|
userEvent.press(green);
|
||||||
|
|
||||||
const modalLabel = await screen.findByText(/number of green chips/i);
|
const modalLabel = await screen.findByText(/number of green chips/i);
|
||||||
expect(modalLabel).toBeDefined();
|
expect(modalLabel).toBeDefined();
|
||||||
|
|
||||||
@ -80,16 +73,27 @@ describe("tests for ChipsSelector", () => {
|
|||||||
TOTAL_CHIPS_COUNT[4],
|
TOTAL_CHIPS_COUNT[4],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
// skip: There is a jest/DOM issue with the button interaction, despite working correctly in-app. Documented to resolve.
|
||||||
it("test dec/inc buttons", async () => {
|
it.skip("test dec/inc buttons", async () => {
|
||||||
rend();
|
rend();
|
||||||
|
|
||||||
|
const blue = screen.getByText(TOTAL_CHIPS_COUNT[3].toString());
|
||||||
|
const black = screen.getByText(TOTAL_CHIPS_COUNT[4].toString());
|
||||||
const decrement = screen.getByRole("button", { name: /-/i });
|
const decrement = screen.getByRole("button", { name: /-/i });
|
||||||
const increment = screen.getByRole("button", { name: /\+/i });
|
const increment = screen.getByRole("button", { name: /\+/i });
|
||||||
|
|
||||||
fireEvent.press(decrement);
|
fireEvent.press(decrement);
|
||||||
expect(mockSetNumberOfChips).toHaveBeenCalledWith(4);
|
fireEvent.press(decrement);
|
||||||
|
|
||||||
|
// Test that elements are removed after fireEvent
|
||||||
|
await waitForElementToBeRemoved(() => blue);
|
||||||
|
await waitForElementToBeRemoved(() => black);
|
||||||
|
|
||||||
fireEvent.press(increment);
|
fireEvent.press(increment);
|
||||||
expect(mockSetNumberOfChips).toHaveBeenCalledWith(4);
|
fireEvent.press(increment);
|
||||||
|
|
||||||
|
// Test that new elements re-appear, correctly
|
||||||
|
const blue1 = screen.getByText(TOTAL_CHIPS_COUNT[3].toString());
|
||||||
|
const black1 = screen.getByText(TOTAL_CHIPS_COUNT[4].toString());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { render, fireEvent } from "@testing-library/react-native";
|
|
||||||
import CurrencySelector from "@/components/CurrencySelector";
|
|
||||||
|
|
||||||
describe("CurrencySelector Component", () => {
|
|
||||||
const mockSetSelectedCurrency = jest.fn();
|
|
||||||
|
|
||||||
test("renders CurrencySelector component correctly", () => {
|
|
||||||
const { getByText, getByTestId } = render(
|
|
||||||
<CurrencySelector
|
|
||||||
selectedCurrency="$"
|
|
||||||
setSelectedCurrency={mockSetSelectedCurrency}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByTestId("currency-picker")).toBeTruthy(); // Check Picker exists
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls setSelectedCurrency when a new currency is selected", () => {
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<CurrencySelector
|
|
||||||
selectedCurrency="$"
|
|
||||||
setSelectedCurrency={mockSetSelectedCurrency}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const picker = getByTestId("currency-picker"); // Get Picker
|
|
||||||
|
|
||||||
fireEvent(picker, "onValueChange", "€"); // Simulate selecting Euro (€)
|
|
||||||
|
|
||||||
expect(mockSetSelectedCurrency).toHaveBeenCalledWith("€");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("updates selected currency when Picker value changes", () => {
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<CurrencySelector
|
|
||||||
selectedCurrency="€"
|
|
||||||
setSelectedCurrency={mockSetSelectedCurrency}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const picker = getByTestId("currency-picker");
|
|
||||||
|
|
||||||
fireEvent(picker, "onValueChange", "$"); // Simulate selecting USD ($)
|
|
||||||
|
|
||||||
expect(mockSetSelectedCurrency).toHaveBeenCalledWith("$");
|
|
||||||
});
|
|
||||||
});
|
|
@ -9,6 +9,7 @@ describe("PlayerSelector Component", () => {
|
|||||||
<PlayerSelector playerCount={4} setPlayerCount={setPlayerCount} />
|
<PlayerSelector playerCount={4} setPlayerCount={setPlayerCount} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(getByText("Select Number of Players:")).toBeTruthy();
|
||||||
expect(getByText("4")).toBeTruthy();
|
expect(getByText("4")).toBeTruthy();
|
||||||
expect(getByRole("button", { name: "-" })).toBeTruthy();
|
expect(getByRole("button", { name: "-" })).toBeTruthy();
|
||||||
expect(getByRole("button", { name: "+" })).toBeTruthy();
|
expect(getByRole("button", { name: "+" })).toBeTruthy();
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
import { COLORS } from "@/styles/styles";
|
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import {
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
StyleSheet,
|
|
||||||
useColorScheme,
|
|
||||||
} from "react-native";
|
|
||||||
|
|
||||||
interface ButtonProps {
|
|
||||||
title: string;
|
|
||||||
onPress: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
size?: "normal" | "small";
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps> = ({
|
|
||||||
title,
|
|
||||||
onPress,
|
|
||||||
disabled,
|
|
||||||
size = "normal",
|
|
||||||
}) => {
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPress}
|
|
||||||
disabled={disabled}
|
|
||||||
accessibilityRole="button"
|
|
||||||
style={[
|
|
||||||
size == "normal" ? styles.button : styles.buttonSmall,
|
|
||||||
{ backgroundColor: colors.PRIMARY },
|
|
||||||
disabled && styles.disabled,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
size == "normal" ? styles.buttonText : styles.buttonTextSmall,
|
|
||||||
{ color: colors.TEXT },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
button: {
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginHorizontal: 5,
|
|
||||||
marginVertical: 5,
|
|
||||||
},
|
|
||||||
buttonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: "bold",
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
buttonSmall: {
|
|
||||||
paddingVertical: 5,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginHorizontal: 2,
|
|
||||||
marginVertical: 2,
|
|
||||||
},
|
|
||||||
buttonTextSmall: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: "bold",
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Button;
|
|
@ -1,49 +0,0 @@
|
|||||||
import styles, { COLORS } from "@/styles/styles";
|
|
||||||
import { PickerItemProps, PickerProps } from "@react-native-picker/picker";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { useColorScheme, View } from "react-native";
|
|
||||||
import { Picker as RNPicker } from "@react-native-picker/picker";
|
|
||||||
|
|
||||||
export const PickerItem: React.FC<PickerItemProps> = (props) => {
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RNPicker.Item
|
|
||||||
style={[
|
|
||||||
styles.pickerItem,
|
|
||||||
{ color: colors.TEXT, backgroundColor: colors.PRIMARY },
|
|
||||||
]}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Picker: React.FC<PickerProps> = (props) => {
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.pickerWrapper}>
|
|
||||||
<RNPicker
|
|
||||||
style={[
|
|
||||||
styles.picker,
|
|
||||||
{
|
|
||||||
color: colors.TEXT,
|
|
||||||
backgroundColor: colors.PRIMARY,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dropdownIconColor={colors.TEXT}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,83 +0,0 @@
|
|||||||
import { View, Text, StyleSheet, useColorScheme } from "react-native";
|
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
|
||||||
import globalStyles, { COLORS } from "@/styles/styles";
|
|
||||||
|
|
||||||
const titleCase = (input: string) =>
|
|
||||||
input
|
|
||||||
.split(" ")
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
// Wrapper container for styling purposes
|
|
||||||
const Section = ({
|
|
||||||
title,
|
|
||||||
iconName,
|
|
||||||
children,
|
|
||||||
orientation = "column",
|
|
||||||
contentStyle = {},
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
iconName: string | any;
|
|
||||||
children: React.JSX.Element;
|
|
||||||
orientation?: "row" | "column";
|
|
||||||
contentStyle?: object;
|
|
||||||
}) => {
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
|
||||||
const colors = useMemo(
|
|
||||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
|
||||||
[darkMode]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<MaterialIcons
|
|
||||||
style={styles.icon}
|
|
||||||
name={iconName}
|
|
||||||
size={30}
|
|
||||||
color={colors.TEXT}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.title, { color: colors.TEXT }]}>
|
|
||||||
{titleCase(title)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
...styles.content,
|
|
||||||
...contentStyle,
|
|
||||||
flexDirection: orientation,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
marginBottom: 20,
|
|
||||||
display: "flex",
|
|
||||||
alignContent: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 5,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
icon: {},
|
|
||||||
title: {
|
|
||||||
...globalStyles.h1,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-evenly",
|
|
||||||
gap: 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Section;
|
|
@ -1,32 +0,0 @@
|
|||||||
import { render, screen } from "@testing-library/react-native";
|
|
||||||
import Section from "../Section";
|
|
||||||
import { Text } from "react-native";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
jest.mock("@expo/vector-icons", () => {
|
|
||||||
const { Text } = require("react-native");
|
|
||||||
return {
|
|
||||||
MaterialIcons: () => <Text testID={"test-icon"}>TestIcon</Text>,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const TITLE = "Select the weather";
|
|
||||||
const rend = () =>
|
|
||||||
render(
|
|
||||||
<Section title={TITLE} iconName={"test-icon"}>
|
|
||||||
<Text>child</Text>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
describe("tests for the Section HOC Component", () => {
|
|
||||||
it("everything expected appears", () => {
|
|
||||||
rend();
|
|
||||||
const title = screen.getByText(/select the weather/i);
|
|
||||||
expect(title).toBeTruthy();
|
|
||||||
|
|
||||||
const child = screen.getByText("child");
|
|
||||||
expect(child).toBeTruthy();
|
|
||||||
|
|
||||||
const icon = screen.getByTestId("test-icon");
|
|
||||||
expect(icon).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
54
i18n/en.json
54
i18n/en.json
@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"translation": {
|
|
||||||
"poker_chips_helper": "Poker Chips Helper",
|
|
||||||
"select_currency": "Select Currency",
|
|
||||||
"usd": "USD ($)",
|
|
||||||
"euro": "Euro (€)",
|
|
||||||
"pound": "Pound (£)",
|
|
||||||
"inr": "INR (₹)",
|
|
||||||
"select_number_of_players": "Select the Number of Players:",
|
|
||||||
"select_buyin_amount": "Select Buy-in Amount:",
|
|
||||||
"custom_buy_in": "Or, enter a custom amount:",
|
|
||||||
"selected_buy_in": "Selected Buy-in:",
|
|
||||||
"none": "None",
|
|
||||||
"pick_an_image": "Pick an image",
|
|
||||||
"take_a_photo": "Take a photo",
|
|
||||||
"chips": "chips",
|
|
||||||
"number_of_chips": "Number of {{color}} chips",
|
|
||||||
"accept": "Accept",
|
|
||||||
"distribution_and_denomination": "Distribution & Denomination",
|
|
||||||
"pot": "Pot",
|
|
||||||
"total_value": "Total Value",
|
|
||||||
"each": "Each",
|
|
||||||
"select_language": "Select Language",
|
|
||||||
"english": "English",
|
|
||||||
"spanish": "Spanish",
|
|
||||||
"settings": "Settings",
|
|
||||||
"camera_permission_required": "Camera permission is required to take a photo.",
|
|
||||||
"how_many_chips_by_color": "How many poker chips are there for each color? Return structured JSON.",
|
|
||||||
"invalid_response": "Invalid response from API.",
|
|
||||||
"failed_to_analyze_image": "Failed to analyze the image.",
|
|
||||||
"state_saved_successfully": "State saved successfully",
|
|
||||||
"state_saved": "State saved to {{slot}}",
|
|
||||||
"state_save_failed": "Failed to save state",
|
|
||||||
"success": "Success",
|
|
||||||
"error": "Error",
|
|
||||||
"failed_to_save_state": "Failed to save state.",
|
|
||||||
"state_loaded_from": "State loaded from",
|
|
||||||
"info": "Info",
|
|
||||||
"warning": "Warning",
|
|
||||||
"no_saved_state_found": "No saved state found.",
|
|
||||||
"automatic_chip_detection": "Automatic Chip Detection",
|
|
||||||
"manual_chip_adjustment": "Manual Chip Adjustment",
|
|
||||||
"save_and_load": "Save & Load",
|
|
||||||
"save_slot_1": "Save\nSlot 1",
|
|
||||||
"save_slot_2": "Save\nSlot 2",
|
|
||||||
"load_slot_1": "Load\nSlot 1",
|
|
||||||
"load_slot_2": "Load\nSlot 2",
|
|
||||||
"please_select_valid_buyin": "Please select a valid buy-in amount",
|
|
||||||
"chip_value_warn": "Be advised that the value of the distributed chips does not equal the buy-in for these inputs.\n\nHowever, results shown are fair to all players",
|
|
||||||
"appearance": "Appearance",
|
|
||||||
"switch_to_dark_mode": "Switch to Dark Mode",
|
|
||||||
"switch_to_light_mode": "Switch to Light Mode"
|
|
||||||
}
|
|
||||||
}
|
|
55
i18n/es.json
55
i18n/es.json
@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"translation": {
|
|
||||||
"poker_chips_helper": "Ayudante de Fichas de Póker",
|
|
||||||
"select_currency": "Seleccionar moneda",
|
|
||||||
"usd": "USD ($)",
|
|
||||||
"euro": "Euro (€)",
|
|
||||||
"pound": "Libra (£)",
|
|
||||||
"inr": "INR (₹)",
|
|
||||||
"select_number_of_players": "Seleccionar número de jugadores:",
|
|
||||||
"select_buyin_amount": "Seleccionar cantidad de buy-in:",
|
|
||||||
"custom_buy_in": "O, ingresa una cantidad personalizada:",
|
|
||||||
"selected_buy_in": "Buy-in seleccionado:",
|
|
||||||
"none": "Ninguno",
|
|
||||||
"pick_an_image": "Elige una imagen",
|
|
||||||
"take_a_photo": "Tomar una foto",
|
|
||||||
"chips": "fichas",
|
|
||||||
"number_of_chips": "Número de {{color}} fichas",
|
|
||||||
"accept": "Aceptar",
|
|
||||||
"distribution_and_denomination": "Distribución y Denominación",
|
|
||||||
"pot": "Olla",
|
|
||||||
"total_value": "Valor total",
|
|
||||||
"each": "Cada",
|
|
||||||
"select_language": "Seleccionar idioma",
|
|
||||||
"english": "Inglés",
|
|
||||||
"spanish": "Español",
|
|
||||||
"settings": "Configuraciones",
|
|
||||||
"camera_permission_required": "Se requiere permiso de cámara para tomar una foto.",
|
|
||||||
"how_many_chips_by_color": "¿Cuántas fichas de póker hay de cada color? Devuelve JSON estructurado.",
|
|
||||||
"invalid_response": "Respuesta no válida de la API.",
|
|
||||||
"failed_to_analyze_image": "No se pudo analizar la imagen",
|
|
||||||
"state_saved_successfully": "Estado guardado con éxito",
|
|
||||||
"state_saved": "Estado guardado en {{slot}}",
|
|
||||||
"state_save_failed": "Error al guardar el estado",
|
|
||||||
"success": "Éxito",
|
|
||||||
"state_saved_to": "Estado guardado en",
|
|
||||||
"error": "Error",
|
|
||||||
"failed_to_save_state": "No se pudo guardar el estado.",
|
|
||||||
"state_loaded_from": "Estado cargado desde",
|
|
||||||
"info": "Información",
|
|
||||||
"warning": "Advertencia",
|
|
||||||
"no_saved_state_found": "No se encontró estado guardado.",
|
|
||||||
"automatic_chip_detection": "Detección automática de fichas",
|
|
||||||
"manual_chip_adjustment": "Ajuste manual de fichas",
|
|
||||||
"save_and_load": "Guardar & Cargar",
|
|
||||||
"save_slot_1": "Guardar\nSlot 1",
|
|
||||||
"save_slot_2": "Guardar\nSlot 2",
|
|
||||||
"load_slot_1": "Cargar\nSlot 1",
|
|
||||||
"load_slot_2": "Cargar\nSlot 2",
|
|
||||||
"please_select_valid_buyin": "Por favor seleccione una cantidad de buy-in válida",
|
|
||||||
"chip_value_warn": "Tenga en cuenta que el valor de las fichas distribuidas no es igual al buy-in para estas entradas.\n\nSin embargo, los resultados que se muestran son justos para todos los jugadores.",
|
|
||||||
"appearance": "Apariencia",
|
|
||||||
"switch_to_dark_mode": "Cambiar a Modo Oscuro",
|
|
||||||
"switch_to_light_mode": "Cambiar a Modo Claro"
|
|
||||||
}
|
|
||||||
}
|
|
22
i18n/i18n.ts
22
i18n/i18n.ts
@ -1,22 +0,0 @@
|
|||||||
import i18n from "i18next";
|
|
||||||
import { initReactI18next } from "react-i18next";
|
|
||||||
import * as Localization from "expo-localization";
|
|
||||||
import en from "./en.json";
|
|
||||||
import es from "./es.json";
|
|
||||||
const resources = {
|
|
||||||
en,
|
|
||||||
es,
|
|
||||||
};
|
|
||||||
|
|
||||||
const detectedLanguage = Localization.locale.split("-")[0] || "en";
|
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
|
||||||
resources,
|
|
||||||
lng: detectedLanguage,
|
|
||||||
fallbackLng: "en",
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
|
1966
package-lock.json
generated
1966
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
95
package.json
95
package.json
@ -5,8 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "expo run:android",
|
"android": "expo start --android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo start --ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"test": "jest --watchAll",
|
"test": "jest --watchAll",
|
||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
@ -14,62 +14,57 @@
|
|||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
},
|
},
|
||||||
"engines": {
|
|
||||||
"node": ">=22.0.0 <23.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "14.0.4",
|
"@babel/preset-typescript": "^7.26.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.1.1",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@react-native-picker/picker": "^2.11.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/bottom-tabs": "7.2.0",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@react-navigation/native": "7.0.14",
|
"expo": "~52.0.31",
|
||||||
"expo": "^52.0.37",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-blur": "14.0.3",
|
"expo-constants": "~17.0.5",
|
||||||
"expo-constants": "17.0.7",
|
"expo-file-system": "~18.0.11",
|
||||||
"expo-file-system": "18.0.11",
|
"expo-font": "~13.0.3",
|
||||||
"expo-font": "13.0.4",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-haptics": "14.0.1",
|
"expo-image-picker": "~16.0.6",
|
||||||
"expo-image-picker": "16.0.6",
|
"expo-linking": "~7.0.5",
|
||||||
"expo-linking": "7.0.5",
|
"expo-router": "~4.0.17",
|
||||||
"expo-localization": "~16.0.1",
|
"expo-splash-screen": "~0.29.21",
|
||||||
"expo-router": "4.0.17",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-splash-screen": "0.29.22",
|
"expo-symbols": "~0.2.2",
|
||||||
"expo-status-bar": "2.0.1",
|
"expo-system-ui": "~4.0.8",
|
||||||
"expo-symbols": "0.2.2",
|
"expo-web-browser": "~14.0.2",
|
||||||
"expo-system-ui": "4.0.8",
|
"metro-react-native-babel-preset": "^0.77.0",
|
||||||
"expo-web-browser": "14.0.2",
|
|
||||||
"i18next": "^24.2.2",
|
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "^15.4.1",
|
"react-native": "^0.76.7",
|
||||||
"react-native": "0.76.7",
|
"react-native-dotenv": "^3.4.11",
|
||||||
"react-native-gesture-handler": "2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-reanimated": "3.16.7",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-web": "0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.12.5"
|
"react-native-webview": "13.12.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.26.9",
|
"@babel/core": "^7.26.9",
|
||||||
"@testing-library/dom": "10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-native": "5.4.3",
|
"@testing-library/jest-native": "^5.4.3",
|
||||||
"@testing-library/react": "16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@testing-library/react-native": "13.0.1",
|
"@testing-library/react-native": "^13.0.1",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-test-renderer": "18.3.0",
|
"@types/react-test-renderer": "^18.3.0",
|
||||||
"eslint": "9.21.0",
|
"eslint": "^9.20.0",
|
||||||
"eslint-config-prettier": "10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-prettier": "5.2.3",
|
"eslint-plugin-prettier": "^5.2.3",
|
||||||
"eslint-plugin-react": "7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-native": "5.0.0",
|
"eslint-plugin-react-native": "^5.0.0",
|
||||||
"jest": "29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expo": "52.0.5",
|
"jest-expo": "~52.0.3",
|
||||||
"jest-fetch-mock": "3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "3.5.2",
|
"prettier": "^3.4.2",
|
||||||
"react-test-renderer": "18.3.1",
|
"react-test-renderer": "18.3.1",
|
||||||
"typescript": "5.7.3"
|
"typescript": "^5.7.3"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import { StyleSheet } from "react-native";
|
|
||||||
|
|
||||||
export const COLORS = {
|
|
||||||
WARNING: "#c79c28",
|
|
||||||
LIGHT: {
|
|
||||||
TEXT: "black",
|
|
||||||
PRIMARY: "lightgrey",
|
|
||||||
SECONDARY: "azure",
|
|
||||||
BACKGROUND: "ghostwhite",
|
|
||||||
DISABLED: "gray",
|
|
||||||
},
|
|
||||||
DARK: {
|
|
||||||
TEXT: "white",
|
|
||||||
PRIMARY: "black",
|
|
||||||
SECONDARY: "teal",
|
|
||||||
BACKGROUND: "dimgrey",
|
|
||||||
DISABLED: "gray",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const GlobalStyles = StyleSheet.create({
|
|
||||||
scrollView: {},
|
|
||||||
scrollViewContent: {
|
|
||||||
padding: 15,
|
|
||||||
},
|
|
||||||
|
|
||||||
container: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
|
|
||||||
h1: { fontSize: 19, fontWeight: "bold" },
|
|
||||||
h2: { fontSize: 18, fontWeight: "normal" },
|
|
||||||
p: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: "#333",
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#ccc",
|
|
||||||
padding: 8,
|
|
||||||
marginVertical: 10,
|
|
||||||
borderRadius: 5,
|
|
||||||
},
|
|
||||||
modal: {},
|
|
||||||
|
|
||||||
button: {
|
|
||||||
backgroundColor: "#007bff",
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 5,
|
|
||||||
},
|
|
||||||
shadow: {
|
|
||||||
textShadowColor: "black",
|
|
||||||
textShadowOffset: { width: 0, height: 0 },
|
|
||||||
textShadowRadius: 10,
|
|
||||||
},
|
|
||||||
picker: {
|
|
||||||
borderRadius: 10,
|
|
||||||
height: 55,
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
pickerItem: {},
|
|
||||||
pickerWrapper: {
|
|
||||||
borderRadius: 10,
|
|
||||||
overflow: "hidden",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default GlobalStyles;
|
|
@ -1,37 +0,0 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
|
||||||
SLOT1: "@poker_state_slot1",
|
|
||||||
SLOT2: "@poker_state_slot2",
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PokerState {
|
|
||||||
playerCount: number;
|
|
||||||
buyInAmount: number | null;
|
|
||||||
numberOfChips: number;
|
|
||||||
totalChipsCount: number[];
|
|
||||||
selectedCurrency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const saveState = async (
|
|
||||||
slot: keyof typeof STORAGE_KEYS,
|
|
||||||
state: PokerState
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await AsyncStorage.setItem(STORAGE_KEYS[slot], JSON.stringify(state));
|
|
||||||
return { success: true, message: `State saved to ${slot}` };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, message: "Failed to save state" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadState = async (
|
|
||||||
slot: keyof typeof STORAGE_KEYS
|
|
||||||
): Promise<PokerState | null> => {
|
|
||||||
try {
|
|
||||||
const storedState = await AsyncStorage.getItem(STORAGE_KEYS[slot]);
|
|
||||||
return storedState ? JSON.parse(storedState) : null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,39 +0,0 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
|
|
||||||
const STORAGE_KEY = "@poker_calculator_state";
|
|
||||||
|
|
||||||
export interface PokerState {
|
|
||||||
playerCount: number;
|
|
||||||
buyInAmount: number | null;
|
|
||||||
numberOfChips: number;
|
|
||||||
totalChipsCount: number[];
|
|
||||||
selectedCurrency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_STATE: PokerState = {
|
|
||||||
playerCount: 0,
|
|
||||||
buyInAmount: null,
|
|
||||||
numberOfChips: 0,
|
|
||||||
totalChipsCount: [],
|
|
||||||
selectedCurrency: "$",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 🔹 Save state with currency
|
|
||||||
export const savePersistentState = async (state: PokerState) => {
|
|
||||||
try {
|
|
||||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
||||||
return { success: true, message: "State saved successfully" };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, message: "Failed to save state" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 🔹 Load state with currency
|
|
||||||
export const loadPersistentState = async (): Promise<PokerState> => {
|
|
||||||
try {
|
|
||||||
const storedState = await AsyncStorage.getItem(STORAGE_KEY);
|
|
||||||
return storedState ? JSON.parse(storedState) : DEFAULT_STATE; // Ensure default values
|
|
||||||
} catch (error) {
|
|
||||||
return DEFAULT_STATE;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,13 +0,0 @@
|
|||||||
import React, { createContext } from "react";
|
|
||||||
|
|
||||||
export interface IAppContext {
|
|
||||||
showSettings: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultContext: IAppContext = {
|
|
||||||
showSettings: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppContext = createContext<IAppContext>(defaultContext);
|
|
||||||
|
|
||||||
export default AppContext;
|
|
@ -1,86 +0,0 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import { saveState, loadState, PokerState } from "@/util/CalculatorState";
|
|
||||||
|
|
||||||
// Mock AsyncStorage
|
|
||||||
jest.mock("@react-native-async-storage/async-storage", () =>
|
|
||||||
require("@react-native-async-storage/async-storage/jest/async-storage-mock")
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("CalculatorState.ts", () => {
|
|
||||||
const mockState: PokerState = {
|
|
||||||
playerCount: 4,
|
|
||||||
buyInAmount: 50,
|
|
||||||
numberOfChips: 5,
|
|
||||||
totalChipsCount: [100, 200, 150, 50, 75],
|
|
||||||
selectedCurrency: "$", // Including selectedCurrency in mockState
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should save state successfully to SLOT1", async () => {
|
|
||||||
await saveState("SLOT1", mockState);
|
|
||||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
||||||
"@poker_state_slot1",
|
|
||||||
JSON.stringify(mockState)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should save state successfully to SLOT2", async () => {
|
|
||||||
await saveState("SLOT2", mockState);
|
|
||||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
||||||
"@poker_state_slot2",
|
|
||||||
JSON.stringify(mockState)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fail to save state if an error occurs", async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(AsyncStorage, "setItem")
|
|
||||||
.mockRejectedValueOnce(new Error("Failed to save"));
|
|
||||||
const result = await saveState("SLOT1", mockState);
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.message).toBe("Failed to save state");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should load state successfully from SLOT1", async () => {
|
|
||||||
await AsyncStorage.setItem("@poker_state_slot1", JSON.stringify(mockState));
|
|
||||||
const result = await loadState("SLOT1");
|
|
||||||
expect(result).toEqual(mockState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should load state successfully from SLOT2", async () => {
|
|
||||||
await AsyncStorage.setItem("@poker_state_slot2", JSON.stringify(mockState));
|
|
||||||
const result = await loadState("SLOT2");
|
|
||||||
expect(result).toEqual(mockState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return null if no state is found in SLOT1", async () => {
|
|
||||||
jest.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(null);
|
|
||||||
const result = await loadState("SLOT1");
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return null if no state is found in SLOT2", async () => {
|
|
||||||
jest.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(null);
|
|
||||||
const result = await loadState("SLOT2");
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return null if an error occurs while loading state from SLOT1", async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(AsyncStorage, "getItem")
|
|
||||||
.mockRejectedValueOnce(new Error("Failed to load"));
|
|
||||||
const result = await loadState("SLOT1");
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return null if an error occurs while loading state from SLOT2", async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(AsyncStorage, "getItem")
|
|
||||||
.mockRejectedValueOnce(new Error("Failed to load"));
|
|
||||||
const result = await loadState("SLOT2");
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,101 +0,0 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import {
|
|
||||||
savePersistentState,
|
|
||||||
loadPersistentState,
|
|
||||||
PokerState,
|
|
||||||
} from "@/util/PersistentState";
|
|
||||||
|
|
||||||
jest.mock("@react-native-async-storage/async-storage", () => ({
|
|
||||||
setItem: jest.fn(),
|
|
||||||
getItem: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("PersistentState.ts", () => {
|
|
||||||
const mockState: PokerState = {
|
|
||||||
playerCount: 4,
|
|
||||||
buyInAmount: 50,
|
|
||||||
numberOfChips: 5,
|
|
||||||
totalChipsCount: [100, 200, 150, 50, 75],
|
|
||||||
selectedCurrency: "$", // Including selectedCurrency in mockState
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should save state successfully", async () => {
|
|
||||||
// Mocking AsyncStorage.setItem to resolve successfully
|
|
||||||
(AsyncStorage.setItem as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
const result = await savePersistentState(mockState);
|
|
||||||
|
|
||||||
// Check that the success flag is true and message is as expected
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.message).toBe("State saved successfully");
|
|
||||||
|
|
||||||
// Check that AsyncStorage.setItem was called with the correct parameters
|
|
||||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
|
||||||
"@poker_calculator_state",
|
|
||||||
JSON.stringify(mockState)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fail to save state if an error occurs", async () => {
|
|
||||||
// Mocking AsyncStorage.setItem to reject with an error
|
|
||||||
(AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(
|
|
||||||
new Error("Failed to save")
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await savePersistentState(mockState);
|
|
||||||
|
|
||||||
// Check that the success flag is false and message is as expected
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
expect(result.message).toBe("Failed to save state");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should load state successfully", async () => {
|
|
||||||
// Mocking AsyncStorage.getItem to resolve with the mockState
|
|
||||||
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(
|
|
||||||
JSON.stringify(mockState)
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await loadPersistentState();
|
|
||||||
|
|
||||||
// Check that the loaded state matches the mockState
|
|
||||||
expect(result).toEqual(mockState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should load default state if no saved state is found", async () => {
|
|
||||||
// Mocking AsyncStorage.getItem to return null (no saved state)
|
|
||||||
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
const result = await loadPersistentState();
|
|
||||||
|
|
||||||
// Check that the default state is returned
|
|
||||||
expect(result).toEqual({
|
|
||||||
playerCount: 0,
|
|
||||||
buyInAmount: null,
|
|
||||||
numberOfChips: 0,
|
|
||||||
totalChipsCount: [],
|
|
||||||
selectedCurrency: "$",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return default state if an error occurs while loading", async () => {
|
|
||||||
// Mocking AsyncStorage.getItem to reject with an error
|
|
||||||
(AsyncStorage.getItem as jest.Mock).mockRejectedValueOnce(
|
|
||||||
new Error("Failed to load")
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await loadPersistentState();
|
|
||||||
|
|
||||||
// Check that the default state is returned on error
|
|
||||||
expect(result).toEqual({
|
|
||||||
playerCount: 0,
|
|
||||||
buyInAmount: null,
|
|
||||||
numberOfChips: 0,
|
|
||||||
totalChipsCount: [],
|
|
||||||
selectedCurrency: "$",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user