Compare commits
34 Commits
djwesty/39
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2b7a9be089 | ||
![]() |
b98d0035d8 | ||
![]() |
cf0b0332a4 | ||
![]() |
e8898d8a60 | ||
![]() |
2e5efda69a | ||
![]() |
e720e2e010 | ||
![]() |
68a91a32ad | ||
![]() |
28addf53a5 | ||
![]() |
533356117a | ||
![]() |
da695be36d | ||
![]() |
63ecde6c99 | ||
![]() |
430750e3d4 | ||
![]() |
84a77ebb58 | ||
![]() |
a07e0df947 | ||
![]() |
292cd7b797 | ||
![]() |
de723a5d8a | ||
![]() |
32ce2f9169 | ||
![]() |
648d815647 | ||
![]() |
abdffcef71 | ||
![]() |
01303b625a | ||
![]() |
ca042b3afb | ||
![]() |
bfa66d5856 | ||
![]() |
f04496bf24 | ||
![]() |
1216e76381 | ||
![]() |
31e0a7a995 | ||
![]() |
c41db0db8b | ||
![]() |
5265730c0c | ||
![]() |
07cbfe7172 | ||
![]() |
8b17649e96 | ||
![]() |
868d9aec54 | ||
![]() |
b7238d11d4 | ||
![]() |
56ce3c9bc2 | ||
![]() |
d27743017c | ||
![]() |
93cc07c28c |
6
app.json
6
app.json
@ -9,7 +9,8 @@
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.anonymous.pokerchipshelper"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
@ -33,7 +34,8 @@
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-localization"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
@ -1,11 +1,17 @@
|
||||
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 React, { useMemo, useState } from "react";
|
||||
import { I18nextProvider, useTranslation } from "react-i18next";
|
||||
import { useColorScheme } from "react-native";
|
||||
|
||||
const RootLayout: React.FC = () => {
|
||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ctx = useMemo<IAppContext>(
|
||||
() => ({
|
||||
showSettings,
|
||||
@ -13,21 +19,39 @@ const RootLayout: React.FC = () => {
|
||||
[showSettings]
|
||||
);
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
||||
const colors = useMemo(
|
||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
||||
[darkMode]
|
||||
);
|
||||
return (
|
||||
<AppContext.Provider value={ctx}>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: true,
|
||||
title: "Poker Chips Helper",
|
||||
headerRight: () => (
|
||||
<MaterialIcons
|
||||
name="settings"
|
||||
onPress={() => setShowSettings(!showSettings)}
|
||||
size={30}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
165
app/index.tsx
165
app/index.tsx
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useContext, useMemo } from "react";
|
||||
import { ScrollView, Alert } from "react-native";
|
||||
import { ScrollView, Alert, useColorScheme, Appearance } from "react-native";
|
||||
import Button from "@/containers/Button";
|
||||
import PlayerSelector from "@/components/PlayerSelector";
|
||||
import BuyInSelector from "@/components/BuyInSelector";
|
||||
@ -7,14 +7,17 @@ import ChipsSelector from "@/components/ChipsSelector";
|
||||
import ChipDistributionSummary from "@/components/ChipDistributionSummary";
|
||||
import ChipDetection from "@/components/ChipDetection";
|
||||
import CurrencySelector from "@/components/CurrencySelector";
|
||||
import { saveState, loadState } from "@/components/CalculatorState";
|
||||
import { saveState, loadState } from "@/util/CalculatorState";
|
||||
import {
|
||||
savePersistentState,
|
||||
loadPersistentState,
|
||||
} from "@/components/PersistentState";
|
||||
import styles from "@/styles/styles";
|
||||
} 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 [playerCount, setPlayerCount] = useState(2);
|
||||
@ -22,23 +25,26 @@ const IndexScreen: React.FC = () => {
|
||||
const [numberOfChips, setNumberOfChips] = useState<number>(5);
|
||||
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 loadPersistentData = async () => {
|
||||
try {
|
||||
console.log("Loading persistent game state...");
|
||||
const savedState = await loadPersistentState();
|
||||
if (savedState) {
|
||||
console.log("Persistent state restored:", savedState);
|
||||
setPlayerCount(savedState.playerCount || 2);
|
||||
setBuyInAmount(savedState.buyInAmount || 20);
|
||||
setNumberOfChips(savedState.numberOfChips || 5);
|
||||
setTotalChipsCount(savedState.totalChipsCount || []);
|
||||
setSelectedCurrency(savedState.selectedCurrency || "$");
|
||||
} else {
|
||||
console.log("No persistent state found, using defaults.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading persistent state:", error);
|
||||
@ -49,7 +55,7 @@ const IndexScreen: React.FC = () => {
|
||||
|
||||
const handleSave = async (slot: "SLOT1" | "SLOT2") => {
|
||||
if (buyInAmount === null) {
|
||||
Alert.alert("Error", "Please select a valid buy-in amount");
|
||||
Alert.alert(i18n.t("error"), i18n.t("please_select_valid_buyin"));
|
||||
return;
|
||||
}
|
||||
const state = {
|
||||
@ -59,56 +65,87 @@ const IndexScreen: React.FC = () => {
|
||||
totalChipsCount,
|
||||
selectedCurrency,
|
||||
};
|
||||
try {
|
||||
await saveState(slot, state);
|
||||
await savePersistentState(state);
|
||||
console.log(`Game state saved to ${slot}:`, state);
|
||||
Alert.alert("Success", `State saved to ${slot}`);
|
||||
} catch (error) {
|
||||
console.error("Error saving state:", error);
|
||||
Alert.alert("Error", "Failed to save state.");
|
||||
}
|
||||
await saveState(slot, state);
|
||||
await savePersistentState(state);
|
||||
Alert.alert(i18n.t("success"), i18n.t("state_saved", { slot }));
|
||||
};
|
||||
|
||||
const handleLoad = async (slot: "SLOT1" | "SLOT2") => {
|
||||
try {
|
||||
const loadedState = await loadState(slot);
|
||||
if (loadedState) {
|
||||
setPlayerCount(loadedState.playerCount);
|
||||
setBuyInAmount(loadedState.buyInAmount);
|
||||
setNumberOfChips(loadedState.numberOfChips);
|
||||
setTotalChipsCount(loadedState.totalChipsCount);
|
||||
setSelectedCurrency(loadedState.selectedCurrency || "$");
|
||||
await savePersistentState(loadedState);
|
||||
console.log(`Game state loaded from ${slot}:`, loadedState);
|
||||
Alert.alert("Success", `State loaded from ${slot}`);
|
||||
} else {
|
||||
Alert.alert("Info", "No saved state found.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading state:", error);
|
||||
Alert.alert("Error", "Failed to load state.");
|
||||
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 {
|
||||
Alert.alert(i18n.t("info"), i18n.t("no_saved_state_found"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = (language: ItemValue, _: any) => {
|
||||
setSelectedLanguage(language.toString());
|
||||
i18n.changeLanguage(language.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollViewContent}
|
||||
>
|
||||
{isSettingsVisible && (
|
||||
<Section title={"Select Currency"} iconName={"attach-money"}>
|
||||
<CurrencySelector
|
||||
selectedCurrency={selectedCurrency}
|
||||
setSelectedCurrency={setSelectedCurrency}
|
||||
/>
|
||||
</Section>
|
||||
<>
|
||||
<Section
|
||||
title={i18n.t("appearance")}
|
||||
iconName={"brightness-4"}
|
||||
orientation="row"
|
||||
>
|
||||
<Button
|
||||
title={
|
||||
darkMode
|
||||
? i18n.t("switch_to_light_mode")
|
||||
: i18n.t("switch_to_dark_mode")
|
||||
}
|
||||
onPress={() =>
|
||||
Appearance.setColorScheme(darkMode ? "light" : "dark")
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title={i18n.t("select_language")}
|
||||
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={"Select the number of players"}
|
||||
title={i18n.t("select_number_of_players")}
|
||||
iconName={"people"}
|
||||
orientation="row"
|
||||
contentStyle={{ justifyContent: "center", gap: 30 }}
|
||||
>
|
||||
<PlayerSelector
|
||||
playerCount={playerCount}
|
||||
@ -116,23 +153,35 @@ const IndexScreen: React.FC = () => {
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title={"Select buy-in amount"} iconName={"monetization-on"}>
|
||||
<Section
|
||||
title={i18n.t("select_buyin_amount")}
|
||||
iconName={"monetization-on"}
|
||||
>
|
||||
<BuyInSelector
|
||||
selectedCurrency={selectedCurrency}
|
||||
setBuyInAmount={setBuyInAmount}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title={"Automatic Chip Detection"} iconName={"camera-alt"}>
|
||||
<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={"Manual Chip Adjustment"} iconName={"account-balance"}>
|
||||
<Section
|
||||
title={i18n.t("manual_chip_adjustment")}
|
||||
iconName={"account-balance"}
|
||||
orientation="row"
|
||||
contentStyle={{ alignItems: "center" }}
|
||||
>
|
||||
<ChipsSelector
|
||||
totalChipsCount={totalChipsCount}
|
||||
setTotalChipsCount={setTotalChipsCount}
|
||||
@ -142,7 +191,7 @@ const IndexScreen: React.FC = () => {
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title={"Distribution & Denomination"}
|
||||
title={i18n.t("distribution_and_denomination")}
|
||||
iconName={"currency-exchange"}
|
||||
>
|
||||
<ChipDistributionSummary
|
||||
@ -153,20 +202,34 @@ const IndexScreen: React.FC = () => {
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title={"Save + Load"} iconName={"save"} orientation="row">
|
||||
<Section
|
||||
title={i18n.t("save_and_load")}
|
||||
iconName={"save"}
|
||||
orientation="row"
|
||||
>
|
||||
<>
|
||||
<Button
|
||||
title={"Save\nSlot 1"}
|
||||
title={i18n.t("save_slot_1")}
|
||||
onPress={() => handleSave("SLOT1")}
|
||||
disabled={buyInAmount === null}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
title={"Save\nSlot 2"}
|
||||
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"
|
||||
/>
|
||||
<Button title={"Load\nSlot 1"} onPress={() => handleLoad("SLOT1")} />
|
||||
<Button title={"Load\nSlot 2"} onPress={() => handleLoad("SLOT2")} />
|
||||
</>
|
||||
</Section>
|
||||
</ScrollView>
|
||||
|
@ -1,14 +1,23 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, TextInput } from "react-native";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { View, Text, TextInput, useColorScheme } from "react-native";
|
||||
import styles, { COLORS } from "@/styles/styles";
|
||||
import Button from "@/containers/Button";
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
interface BuyInSelectorProps {
|
||||
setBuyInAmount: React.Dispatch<React.SetStateAction<number>>;
|
||||
selectedCurrency: string; // Accept selectedCurrency as a prop
|
||||
selectedCurrency: string;
|
||||
}
|
||||
|
||||
const defaultBuyInOptions = [10, 25, 50];
|
||||
const MIN = 1;
|
||||
const MAX = 200;
|
||||
|
||||
const parseRoundClamp = (num: string): number => {
|
||||
const parsed = parseFloat(num);
|
||||
const rounded = Math.round(parsed);
|
||||
return Math.min(Math.max(rounded, MIN), MAX);
|
||||
};
|
||||
|
||||
const BuyInSelector: React.FC<BuyInSelectorProps> = ({
|
||||
setBuyInAmount,
|
||||
@ -16,11 +25,17 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({
|
||||
}) => {
|
||||
const [customAmount, setCustomAmount] = useState("");
|
||||
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 numericValue = parseFloat(value);
|
||||
const numericValue = parseRoundClamp(value);
|
||||
if (!isNaN(numericValue) && numericValue >= 0) {
|
||||
setCustomAmount(value);
|
||||
setCustomAmount(numericValue.toString());
|
||||
setBuyInAmountState(numericValue);
|
||||
setBuyInAmount(numericValue);
|
||||
} else {
|
||||
@ -42,25 +57,27 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({
|
||||
{defaultBuyInOptions.map((amount) => (
|
||||
<Button
|
||||
key={amount}
|
||||
color={buyInAmount === amount ? COLORS.PRIMARY : COLORS.SECONDARY}
|
||||
onPress={() => handleBuyInSelection(amount)}
|
||||
title={`${selectedCurrency} ${amount}`}
|
||||
></Button>
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={styles.p}>Or enter a custom amount:</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
style={[styles.input, { color: colors.TEXT }]}
|
||||
placeholderTextColor={colors.TEXT}
|
||||
value={customAmount}
|
||||
maxLength={3}
|
||||
onChangeText={handleCustomAmountChange}
|
||||
placeholder="Enter custom buy-in"
|
||||
placeholder={`${i18n.t("custom_buy_in")} ${MIN} - ${MAX}`}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
|
||||
<Text style={styles.h2}>
|
||||
Selected Buy-in:{" "}
|
||||
{buyInAmount !== null ? `${selectedCurrency} ${buyInAmount}` : "None"}
|
||||
<Text style={[styles.h2, { color: colors.TEXT }]}>
|
||||
{`${i18n.t("selected_buy_in")} `}
|
||||
{buyInAmount !== null
|
||||
? `${selectedCurrency} ${buyInAmount}`
|
||||
: i18n.t("none")}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
|
@ -1,18 +1,22 @@
|
||||
import React, { useState } from "react";
|
||||
import { Image, ActivityIndicator, Text, View } from "react-native";
|
||||
import Button from "@/containers/Button";
|
||||
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
const ChipDetection = ({
|
||||
updateChipCount,
|
||||
}: {
|
||||
updateChipCount: () => void;
|
||||
updateChipCount: (chipData: Record<string, number>) => void;
|
||||
}) => {
|
||||
const [imageUri, setImageUri] = useState(null);
|
||||
const [imageUri, setImageUri] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [lastDetectedChips, setLastDetectedChips] = useState({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastDetectedChips, setLastDetectedChips] = useState<
|
||||
Record<string, number>
|
||||
>({});
|
||||
|
||||
const chipColors = ["white", "red", "green", "blue", "black"];
|
||||
|
||||
const requestCameraPermissions = async () => {
|
||||
const cameraPermission = await ImagePicker.requestCameraPermissionsAsync();
|
||||
@ -26,16 +30,16 @@ const ChipDetection = ({
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
if (!result.canceled && result.assets && result.assets.length > 0) {
|
||||
setImageUri(result.assets[0].uri);
|
||||
await processImage(result.assets[0].base64);
|
||||
await processImage(result.assets[0].base64 as string);
|
||||
}
|
||||
};
|
||||
|
||||
const takePhoto = async () => {
|
||||
const hasPermission = await requestCameraPermissions();
|
||||
if (!hasPermission) {
|
||||
setError("Camera permission is required to take a photo.");
|
||||
setError(i18n.t("camera_permission_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -44,25 +48,25 @@ const ChipDetection = ({
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
if (!result.canceled && result.assets && result.assets.length > 0) {
|
||||
setImageUri(result.assets[0].uri);
|
||||
await processImage(result.assets[0].base64);
|
||||
await processImage(result.assets[0].base64 as string);
|
||||
}
|
||||
};
|
||||
|
||||
const processImage = async (base64Image) => {
|
||||
const processImage = async (base64Image: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(process.env.EXPO_PUBLIC_API_URL, {
|
||||
const response = await fetch(process.env.EXPO_PUBLIC_API_URL as string, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.EXPO_PUBLIC_API_KEY}`, // Use environment variable for API key
|
||||
Authorization: `Bearer ${process.env.EXPO_PUBLIC_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.EXPO_PUBLIC_MODEL_NAME, // Use environment variable for model name
|
||||
model: process.env.EXPO_PUBLIC_MODEL_NAME,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
@ -90,42 +94,60 @@ const ChipDetection = ({
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.choices || !result.choices[0].message) {
|
||||
throw new Error("Invalid response from API.");
|
||||
throw new Error(i18n.t("invalid_response"));
|
||||
}
|
||||
|
||||
const rawContent = result.choices[0].message.content.trim();
|
||||
const cleanJSON = rawContent.replace(/```json|```/g, "").trim();
|
||||
const parsedData: Record<string, number> = JSON.parse(cleanJSON);
|
||||
|
||||
const parsedData = JSON.parse(cleanJSON);
|
||||
|
||||
const filteredData = Object.fromEntries(
|
||||
Object.entries(parsedData).filter(([_, count]) => count > 0)
|
||||
);
|
||||
const filteredData = Object.entries(parsedData)
|
||||
.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);
|
||||
updateChipCount(filteredData);
|
||||
} catch (error) {
|
||||
setError("Failed to analyze the image.");
|
||||
setError(i18n.t("failed_to_analyze_image"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-evenly" }}>
|
||||
<Button title="Pick an Image" onPress={pickImage} />
|
||||
<Button title="Take a Photo" onPress={takePhoto} />
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
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 && (
|
||||
<Image
|
||||
source={{ uri: imageUri }}
|
||||
style={{ width: 300, height: 300, marginTop: 10 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{loading && <ActivityIndicator size="large" color="blue" />}
|
||||
|
||||
{error && <Text style={{ color: "red", marginTop: 10 }}>{error}</Text>}
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,16 +1,20 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { View, Text, Alert } from "react-native";
|
||||
import { ColorValue } from "react-native";
|
||||
import styles from "@/styles/styles";
|
||||
import i18n from "@/i18n/i18n";
|
||||
import styles, { COLORS } from "@/styles/styles";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
|
||||
interface ChipDistributionSummaryProps {
|
||||
playerCount: number;
|
||||
buyInAmount: number;
|
||||
totalChipsCount: number[];
|
||||
colors?: ColorValue[];
|
||||
selectedCurrency: string; // Add the selectedCurrency as a prop here
|
||||
selectedCurrency: string;
|
||||
}
|
||||
|
||||
const reverseFib: number[] = [8, 5, 3, 2, 1];
|
||||
|
||||
const ChipDistributionSummary = ({
|
||||
playerCount,
|
||||
buyInAmount,
|
||||
@ -19,11 +23,15 @@ const ChipDistributionSummary = ({
|
||||
selectedCurrency = "$",
|
||||
}: ChipDistributionSummaryProps) => {
|
||||
const validDenominations: validDenomination[] = [
|
||||
0.05, 0.1, 0.25, 0.5, 1, 2, 2.5, 5, 10, 20, 50, 100,
|
||||
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
|
||||
@ -35,120 +43,112 @@ const ChipDistributionSummary = ({
|
||||
| 5
|
||||
| 10
|
||||
| 20
|
||||
| 25
|
||||
| 50
|
||||
| 100;
|
||||
|
||||
const findFloorDenomination = (target: number): validDenomination => {
|
||||
let current: validDenomination = validDenominations[0];
|
||||
validDenominations.forEach((value, index) => {
|
||||
validDenominations.forEach((value, _) => {
|
||||
if (value < target) current = value;
|
||||
});
|
||||
return current;
|
||||
};
|
||||
|
||||
const maxDenomination = useMemo(() => {
|
||||
if (totalChipsCount.length > 3) {
|
||||
return findFloorDenomination(buyInAmount / 3);
|
||||
} else {
|
||||
return findFloorDenomination(buyInAmount / 4);
|
||||
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;
|
||||
}
|
||||
}, [totalChipsCount]);
|
||||
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 < totalChipsCount.length; i++) {
|
||||
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]
|
||||
);
|
||||
|
||||
const redenominate = useCallback(
|
||||
(
|
||||
invalidDenomination: validDenomination[],
|
||||
shuffleIndex: number
|
||||
): validDenomination[] => {
|
||||
let moved = false;
|
||||
const newDenomination: validDenomination[] = [];
|
||||
for (let i = invalidDenomination.length - 1; i >= 0; i--) {
|
||||
if (i > shuffleIndex) {
|
||||
newDenomination.push(invalidDenomination[i]);
|
||||
} else if (i == shuffleIndex) {
|
||||
newDenomination.push(invalidDenomination[i]);
|
||||
} else if (i < shuffleIndex && !moved) {
|
||||
const nextLowestDenominationIndex = Math.max(
|
||||
validDenominations.indexOf(invalidDenomination[i]) - 1,
|
||||
0
|
||||
);
|
||||
newDenomination.push(validDenominations[nextLowestDenominationIndex]);
|
||||
moved = true;
|
||||
} else {
|
||||
newDenomination.push(invalidDenomination[i]);
|
||||
}
|
||||
}
|
||||
newDenomination.reverse();
|
||||
return newDenomination;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Dynamically set denominations and distributions from changing inputs
|
||||
useEffect(() => {
|
||||
let testDenomination: validDenomination[] = [];
|
||||
const numColors = totalChipsCount.length;
|
||||
const testDistribution: number[] = [];
|
||||
for (let i = 0; i < numColors; ++i) {
|
||||
testDistribution.push(0);
|
||||
}
|
||||
const totalNumColors = totalChipsCount.length;
|
||||
|
||||
// Start with max denominations, then push on the next adjacent lower denomination
|
||||
testDenomination.push(maxDenomination);
|
||||
let currentDenominationIndex: number =
|
||||
validDenominations.indexOf(maxDenomination);
|
||||
for (let i = numColors - 2; i >= 0; i = i - 1) {
|
||||
for (
|
||||
let i = totalNumColors - 2;
|
||||
i >= 0 && currentDenominationIndex > 0;
|
||||
i = i - 1
|
||||
) {
|
||||
currentDenominationIndex -= 1;
|
||||
const currentDemoniation = validDenominations[currentDenominationIndex];
|
||||
testDenomination.push(currentDemoniation);
|
||||
}
|
||||
testDenomination.reverse();
|
||||
let numColors = testDenomination.length;
|
||||
|
||||
let remainingValue = buyInAmount;
|
||||
let fail = true;
|
||||
let failCount = 0;
|
||||
while (fail && failCount < 1) {
|
||||
let stop = false;
|
||||
while (remainingValue > 0 && !stop) {
|
||||
let distributed = false;
|
||||
for (let i = numColors - 1; i >= 0; i = i - 1) {
|
||||
if (testDistribution[i] < maxPossibleDistribution[i]) {
|
||||
if (remainingValue >= testDenomination[i]) {
|
||||
testDistribution[i] = testDistribution[i] + 1;
|
||||
remainingValue = remainingValue - testDenomination[i];
|
||||
distributed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (distributed == false) {
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
if (remainingValue !== 0) {
|
||||
const redenominateIndex = failCount % numColors;
|
||||
testDenomination = redenominate(testDenomination, redenominateIndex);
|
||||
failCount += 1;
|
||||
fail = true;
|
||||
} else {
|
||||
fail = false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
setDenominations(testDenomination);
|
||||
setDistributions(testDistribution);
|
||||
}, [totalChipsCount, maxDenomination, buyInAmount, playerCount]);
|
||||
@ -156,26 +156,41 @@ const ChipDistributionSummary = ({
|
||||
return (
|
||||
<>
|
||||
<View style={styles.container}>
|
||||
{totalChipsCount.map((_, index) => (
|
||||
<View style={{ flexDirection: "row" }} key={index}>
|
||||
<Text
|
||||
style={{
|
||||
...styles.h2,
|
||||
color: colors[index],
|
||||
...(colors[index] === "white" && styles.shadow),
|
||||
}}
|
||||
>
|
||||
{`${distributions[index]} chips: ${selectedCurrency}${denominations[index]} each`}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{distributions.map((distribution, index) => {
|
||||
return (
|
||||
distribution > 0 && (
|
||||
<View style={{ flexDirection: "row" }} key={index}>
|
||||
<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>
|
||||
{round(totalValue) !== buyInAmount && (
|
||||
<MaterialIcons
|
||||
name="warning"
|
||||
size={20}
|
||||
color={COLORS.WARNING}
|
||||
onPress={showAlert}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.p}>
|
||||
Total Value: {selectedCurrency} {totalValue}
|
||||
</Text>
|
||||
<Text style={styles.p}>
|
||||
{selectedCurrency} {potValue} Pot
|
||||
{selectedCurrency} {potValue} {i18n.t("pot")}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
|
@ -11,8 +11,10 @@ import {
|
||||
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 defaults = [100, 50, 50, 50, 50];
|
||||
|
||||
const ChipInputModal = ({
|
||||
showModal,
|
||||
@ -28,12 +30,11 @@ const ChipInputModal = ({
|
||||
const color: ColorValue = useMemo(() => showModal[1], [showModal]);
|
||||
const colorIdx = useMemo(() => colors.indexOf(color), [color]);
|
||||
|
||||
const [value, setValue] = useState<number | undefined>(); // value may be undefined initially
|
||||
const [value, setValue] = useState<number | undefined>();
|
||||
|
||||
// Reset the color value when the specific color this modal is for, changes. The same modal is shared/reused in all cases.
|
||||
useEffect(() => {
|
||||
setValue(totalChipsCount[colorIdx]);
|
||||
}, [colorIdx]);
|
||||
}, [colorIdx, totalChipsCount]);
|
||||
|
||||
const shadow = useMemo(() => color === "white", [color]);
|
||||
|
||||
@ -48,7 +49,9 @@ const ChipInputModal = ({
|
||||
{value !== undefined && (
|
||||
<>
|
||||
<Text style={styles.h2}>
|
||||
Number of {showModal[1]?.toString()} chips
|
||||
{i18n.t("number_of_chips", {
|
||||
color: showModal[1]?.toString(),
|
||||
})}{" "}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={{
|
||||
@ -67,7 +70,7 @@ const ChipInputModal = ({
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
title="Accept"
|
||||
title={i18n.t("accept")}
|
||||
onPress={() => {
|
||||
update(showModal[1], Number.isNaN(value) ? 0 : value);
|
||||
setShowModal([false, color]);
|
||||
@ -123,12 +126,12 @@ const ChipsSelector = ({
|
||||
false,
|
||||
colors[0],
|
||||
]);
|
||||
|
||||
const colorsUsed = useMemo(
|
||||
() => colors.filter((v, i) => i < numberOfChips),
|
||||
() => colors.slice(0, numberOfChips),
|
||||
[numberOfChips]
|
||||
);
|
||||
|
||||
// Callback for ChipInputModal to update the chips in the parents state.
|
||||
const update = useCallback(
|
||||
(color: ColorValue, count: number) => {
|
||||
const newTotalChipsCount = totalChipsCount.slice();
|
||||
@ -136,20 +139,18 @@ const ChipsSelector = ({
|
||||
newTotalChipsCount[colorIndex] = count;
|
||||
setTotalChipsCount(newTotalChipsCount);
|
||||
},
|
||||
[numberOfChips, totalChipsCount, setTotalChipsCount]
|
||||
[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(() => {
|
||||
if (numberOfChips !== totalChipsCount.length) {
|
||||
let newTotalChipsCount = totalChipsCount.slice();
|
||||
|
||||
if (numberOfChips < totalChipsCount.length) {
|
||||
newTotalChipsCount = newTotalChipsCount.slice(0, numberOfChips);
|
||||
} else if (numberOfChips > totalChipsCount.length) {
|
||||
for (let colorIndex = 0; colorIndex < numberOfChips; ++colorIndex) {
|
||||
if (colorIndex >= newTotalChipsCount.length) {
|
||||
const defaultTotal = 100 - colorIndex * 20;
|
||||
const defaultTotal = defaults[colorIndex];
|
||||
newTotalChipsCount.push(defaultTotal);
|
||||
}
|
||||
}
|
||||
@ -160,32 +161,33 @@ const ChipsSelector = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title="-"
|
||||
onPress={() => {
|
||||
setNumberOfChips(Math.max(1, numberOfChips - 1));
|
||||
}}
|
||||
disabled={numberOfChips === 1}
|
||||
/>
|
||||
<View style={[styles.container, { flexDirection: "row" }]}>
|
||||
{colorsUsed.map((color) => (
|
||||
<Chip
|
||||
key={color.toString()}
|
||||
color={color}
|
||||
count={totalChipsCount[colors.indexOf(color)] ?? 0}
|
||||
setShowModal={setShowModal}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<View style={[styles.container, { flexDirection: "row" }]}>
|
||||
<Button
|
||||
title="-"
|
||||
onPress={() => {
|
||||
setNumberOfChips(Math.max(1, numberOfChips - 1));
|
||||
}}
|
||||
disabled={numberOfChips == 1}
|
||||
/>
|
||||
<Button
|
||||
title="+"
|
||||
onPress={() => {
|
||||
setNumberOfChips(Math.min(5, numberOfChips + 1));
|
||||
}}
|
||||
disabled={numberOfChips == 5}
|
||||
/>
|
||||
{colorsUsed.map((color) => {
|
||||
const chipCount = totalChipsCount[colors.indexOf(color)] ?? 0;
|
||||
return (
|
||||
<Chip
|
||||
key={color.toString()}
|
||||
color={color}
|
||||
count={chipCount}
|
||||
setShowModal={setShowModal}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<Button
|
||||
title="+"
|
||||
onPress={() => {
|
||||
setNumberOfChips(Math.min(5, numberOfChips + 1));
|
||||
}}
|
||||
disabled={numberOfChips === 5}
|
||||
/>
|
||||
|
||||
<ChipInputModal
|
||||
showModal={showModal}
|
||||
@ -197,34 +199,4 @@ const ChipsSelector = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles1 = 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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Picker } from "@react-native-picker/picker";
|
||||
import styles from "@/styles/styles";
|
||||
import i18n from "@/i18n/i18n";
|
||||
import { Picker, PickerItem } from "@/containers/Picker";
|
||||
|
||||
interface CurrencySelectorProps {
|
||||
selectedCurrency: string;
|
||||
@ -15,14 +15,13 @@ const CurrencySelector: React.FC<CurrencySelectorProps> = ({
|
||||
<>
|
||||
<Picker
|
||||
selectedValue={selectedCurrency}
|
||||
onValueChange={(itemValue) => setSelectedCurrency(itemValue)}
|
||||
style={styles.picker}
|
||||
onValueChange={(itemValue) => setSelectedCurrency(itemValue.toString())}
|
||||
testID="currency-picker" // ✅ Add testID here
|
||||
>
|
||||
<Picker.Item label="USD ($)" value="$" />
|
||||
<Picker.Item label="Euro (€)" value="€" />
|
||||
<Picker.Item label="Pound (£)" value="£" />
|
||||
<Picker.Item label="INR (₹)" value="₹" />
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import React, { useMemo } from "react";
|
||||
import { View, Text, useColorScheme } from "react-native";
|
||||
import Button from "@/containers/Button";
|
||||
import styles from "@/styles/styles";
|
||||
import styles, { COLORS } from "@/styles/styles";
|
||||
|
||||
interface PlayerSelectorProps {
|
||||
playerCount: number;
|
||||
setPlayerCount: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const MIN = 2;
|
||||
const MAX = 8;
|
||||
|
||||
const PlayerSelector: React.FC<PlayerSelectorProps> = ({
|
||||
playerCount,
|
||||
setPlayerCount,
|
||||
@ -20,23 +22,27 @@ const PlayerSelector: React.FC<PlayerSelectorProps> = ({
|
||||
const decreasePlayers = () => {
|
||||
if (playerCount > MIN) setPlayerCount(playerCount - 1);
|
||||
};
|
||||
const colorScheme = useColorScheme();
|
||||
const darkMode = useMemo(() => colorScheme === "dark", [colorScheme]);
|
||||
const colors = useMemo(
|
||||
() => (darkMode ? COLORS.DARK : COLORS.LIGHT),
|
||||
[darkMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.h1}>{playerCount}</Text>
|
||||
<View style={{ flexDirection: "row", gap: 10 }}>
|
||||
<Button
|
||||
title="-"
|
||||
onPress={decreasePlayers}
|
||||
disabled={playerCount <= MIN}
|
||||
/>
|
||||
<Button
|
||||
title="+"
|
||||
onPress={increasePlayers}
|
||||
disabled={playerCount >= MAX}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
||||
<Button
|
||||
title="-"
|
||||
onPress={decreasePlayers}
|
||||
disabled={playerCount <= MIN}
|
||||
/>
|
||||
<Text style={[styles.h1, { color: colors.TEXT }]}>{playerCount}</Text>
|
||||
<Button
|
||||
title="+"
|
||||
onPress={increasePlayers}
|
||||
disabled={playerCount >= MAX}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React from "react";
|
||||
import { fireEvent, render } from "@testing-library/react-native";
|
||||
import BuyInSelector from "@/components/BuyInSelector";
|
||||
|
||||
// Mocking vector icons to prevent errors
|
||||
jest.mock("@expo/vector-icons", () => {
|
||||
const { Text } = require("react-native");
|
||||
return {
|
||||
@ -10,95 +11,144 @@ jest.mock("@expo/vector-icons", () => {
|
||||
});
|
||||
|
||||
describe("BuyInSelector Component", () => {
|
||||
let setBuyInAmount;
|
||||
let getByText;
|
||||
let getByPlaceholderText;
|
||||
let setBuyInAmount: jest.Mock;
|
||||
|
||||
// Render the component with the necessary props
|
||||
// Render the component and return query methods
|
||||
const renderComponent = (selectedCurrency = "$") => {
|
||||
const result = render(
|
||||
const utils = render(
|
||||
<BuyInSelector
|
||||
setBuyInAmount={setBuyInAmount}
|
||||
selectedCurrency={selectedCurrency}
|
||||
/>
|
||||
);
|
||||
getByText = result.getByText;
|
||||
getByPlaceholderText = result.getByPlaceholderText;
|
||||
return {
|
||||
...utils,
|
||||
getByText: utils.getByText,
|
||||
getByPlaceholderText: utils.getByPlaceholderText,
|
||||
queryByText: utils.queryByText,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setBuyInAmount = jest.fn();
|
||||
renderComponent(); // Render with default currency
|
||||
});
|
||||
|
||||
it("renders the buy-in options and input correctly", () => {
|
||||
const { getByText, getByPlaceholderText, queryByText } = renderComponent();
|
||||
|
||||
expect(getByText("$ 10")).toBeTruthy();
|
||||
expect(getByText("$ 25")).toBeTruthy();
|
||||
expect(getByText("$ 50")).toBeTruthy();
|
||||
expect(getByPlaceholderText("Enter custom buy-in")).toBeTruthy();
|
||||
expect(getByText("Selected Buy-in: None")).toBeTruthy(); // Check default selection
|
||||
expect(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200")
|
||||
).toBeTruthy();
|
||||
expect(queryByText(/Selected Buy-in:.*None/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sets a predefined buy-in amount correctly", () => {
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
fireEvent.press(getByText("$ 25"));
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
||||
});
|
||||
|
||||
it("sets a custom buy-in amount correctly", () => {
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "100");
|
||||
const { getByPlaceholderText } = renderComponent();
|
||||
|
||||
fireEvent.changeText(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
||||
"100"
|
||||
);
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(100);
|
||||
});
|
||||
|
||||
it("resets custom amount if invalid input is entered", () => {
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "-10");
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(25); // Assuming 25 is the default
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "abc");
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(25); // Reset to default
|
||||
it("bound and validate custom amount if invalid input is entered", () => {
|
||||
const { getByPlaceholderText } = renderComponent();
|
||||
|
||||
fireEvent.changeText(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
||||
"-10"
|
||||
);
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(1); // Min value
|
||||
|
||||
fireEvent.changeText(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
||||
"abc"
|
||||
);
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("clears the custom amount when selecting a predefined option", () => {
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "100");
|
||||
const { getByPlaceholderText, getByText } = renderComponent();
|
||||
|
||||
fireEvent.changeText(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
||||
"100"
|
||||
);
|
||||
fireEvent.press(getByText("$ 50"));
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(50);
|
||||
});
|
||||
|
||||
it("handles valid and invalid input for custom amount correctly", () => {
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "75");
|
||||
const { getByPlaceholderText } = renderComponent();
|
||||
|
||||
fireEvent.changeText(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
||||
"75"
|
||||
);
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(75);
|
||||
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "-5");
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
||||
fireEvent.changeText(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
||||
"-5"
|
||||
);
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(1);
|
||||
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "abc");
|
||||
fireEvent.changeText(
|
||||
getByPlaceholderText("Or, enter a custom amount: 1 - 200"),
|
||||
"abc"
|
||||
);
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
||||
});
|
||||
|
||||
it("triggers state update every time a buy-in option is clicked, even if it's the same", () => {
|
||||
fireEvent.press(getByText("$ 25")); // First click
|
||||
fireEvent.press(getByText("$ 25")); // Clicking the same option again
|
||||
expect(setBuyInAmount).toHaveBeenCalledTimes(2); // Expect it to be called twice
|
||||
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 input = getByPlaceholderText("Enter custom buy-in");
|
||||
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); // Assuming 25 is the default
|
||||
expect(setBuyInAmount).toHaveBeenCalledWith(25);
|
||||
});
|
||||
|
||||
it("updates state correctly when selecting predefined buy-in after entering a custom amount", () => {
|
||||
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "200");
|
||||
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", () => {
|
||||
renderComponent("€"); // Test with a different currency
|
||||
const { getByText, queryByText } = renderComponent("€");
|
||||
|
||||
expect(getByText("€ 10")).toBeTruthy();
|
||||
expect(getByText("€ 25")).toBeTruthy();
|
||||
expect(getByText("€ 50")).toBeTruthy();
|
||||
expect(getByText("Selected Buy-in: None")).toBeTruthy();
|
||||
expect(queryByText(/Selected Buy-in:.*None/i)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -17,11 +17,10 @@ jest.mock("expo-image-picker", () => ({
|
||||
describe("ChipDetection", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
jest.spyOn(global, "fetch").mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
@ -34,20 +33,27 @@ describe("ChipDetection", () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders correctly", () => {
|
||||
const { getByText } = render(
|
||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||
);
|
||||
expect(getByText("Pick an Image")).toBeTruthy();
|
||||
expect(getByText("Take a Photo")).toBeTruthy();
|
||||
|
||||
expect(getByText(/pick an image/i)).toBeTruthy();
|
||||
expect(getByText(/take a photo/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("picks an image from the library", async () => {
|
||||
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
|
||||
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
|
||||
canceled: false,
|
||||
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
||||
});
|
||||
@ -55,16 +61,18 @@ describe("ChipDetection", () => {
|
||||
const { getByText } = render(
|
||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||
);
|
||||
fireEvent.press(getByText("Pick an Image"));
|
||||
fireEvent.press(getByText(/pick an image/i));
|
||||
|
||||
await waitFor(() => expect(mockUpdateChipCount).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("takes a photo with the camera", async () => {
|
||||
ImagePicker.requestCameraPermissionsAsync.mockResolvedValueOnce({
|
||||
(
|
||||
ImagePicker.requestCameraPermissionsAsync as jest.Mock
|
||||
).mockResolvedValueOnce({
|
||||
granted: true,
|
||||
});
|
||||
ImagePicker.launchCameraAsync.mockResolvedValueOnce({
|
||||
(ImagePicker.launchCameraAsync as jest.Mock).mockResolvedValueOnce({
|
||||
canceled: false,
|
||||
assets: [{ uri: "test-camera-uri", base64: "test-camera-base64" }],
|
||||
});
|
||||
@ -72,84 +80,94 @@ describe("ChipDetection", () => {
|
||||
const { getByText } = render(
|
||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||
);
|
||||
fireEvent.press(getByText("Take a Photo"));
|
||||
fireEvent.press(getByText(/take a photo/i));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockUpdateChipCount).toHaveBeenCalledWith({ red: 5, green: 3 })
|
||||
expect(mockUpdateChipCount).toHaveBeenCalledWith({
|
||||
red: 5,
|
||||
green: 3,
|
||||
blue: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("handles camera permission denied", async () => {
|
||||
ImagePicker.requestCameraPermissionsAsync.mockResolvedValueOnce({
|
||||
(
|
||||
ImagePicker.requestCameraPermissionsAsync as jest.Mock
|
||||
).mockResolvedValueOnce({
|
||||
granted: false,
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||
);
|
||||
fireEvent.press(getByText("Take a Photo"));
|
||||
fireEvent.press(getByText(/take a photo/i));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getByText("Camera permission is required to take a photo.")
|
||||
getByText(/camera permission is required to take a photo/i)
|
||||
).toBeTruthy()
|
||||
);
|
||||
});
|
||||
|
||||
it("displays error message on image processing failure", async () => {
|
||||
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
|
||||
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
|
||||
canceled: false,
|
||||
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
||||
});
|
||||
|
||||
global.fetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ choices: [] }),
|
||||
})
|
||||
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"));
|
||||
fireEvent.press(getByText(/pick an image/i));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByText("Failed to analyze the image.")).toBeTruthy()
|
||||
expect(getByText(/failed to analyze the image/i)).toBeTruthy()
|
||||
);
|
||||
});
|
||||
|
||||
it("handles valid API response correctly", async () => {
|
||||
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
|
||||
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
|
||||
canceled: false,
|
||||
assets: [{ uri: "test-uri", base64: "test-base64" }],
|
||||
});
|
||||
|
||||
global.fetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
jest.spyOn(global, "fetch").mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: JSON.stringify({ red: 5, green: 3 }),
|
||||
content: JSON.stringify({ red: 5, green: 3, blue: 0 }),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const { getByText } = render(
|
||||
<ChipDetection updateChipCount={mockUpdateChipCount} />
|
||||
);
|
||||
fireEvent.press(getByText("Pick an Image"));
|
||||
fireEvent.press(getByText(/pick an image/i));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockUpdateChipCount).toHaveBeenCalledWith({
|
||||
red: 5,
|
||||
green: 3,
|
||||
blue: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -1,16 +1,23 @@
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react-native";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
import { fireEvent, render } from "@testing-library/react-native";
|
||||
import ChipDistributionSummary from "../ChipDistributionSummary";
|
||||
|
||||
jest.mock("@expo/vector-icons", () => {
|
||||
const { Text } = require("react-native");
|
||||
return {
|
||||
MaterialIcons: () => <Text>TestIcon</Text>,
|
||||
};
|
||||
});
|
||||
|
||||
describe("ChipDistributionSummary Component", () => {
|
||||
test("renders correctly with valid data", () => {
|
||||
const playerCount = 4;
|
||||
const totalChipsCount = [100, 80, 60, 40, 20];
|
||||
const colors = ["WHITE", "RED", "GREEN", "BLUE", "BLACK"];
|
||||
const buyInAmount = 20;
|
||||
|
||||
const expectedDistribution = [2, 2, 1, 2, 2];
|
||||
const expectedDenominations = [0.5, 1, 2, 2.5, 5];
|
||||
const expectedDistribution = [16, 12, 8, 6, 2];
|
||||
const expectedDenominations = [0.05, 0.1, 0.25, 1, 5];
|
||||
|
||||
const { getByText } = render(
|
||||
<ChipDistributionSummary
|
||||
@ -23,52 +30,30 @@ describe("ChipDistributionSummary Component", () => {
|
||||
|
||||
expectedDistribution.forEach((count, index) => {
|
||||
const regex = new RegExp(
|
||||
`^${count}\\s+chips:\\s+\\$${expectedDenominations[index]}\\s+each$`
|
||||
`^${count}\\s+chips:\\s+\\$${expectedDenominations[index]}\\s+Each$`,
|
||||
"i"
|
||||
);
|
||||
expect(getByText(regex)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("renders fallback message when no valid distribution", () => {
|
||||
test("renders warning message when needed", async () => {
|
||||
const { getByText } = render(
|
||||
<ChipDistributionSummary
|
||||
playerCount={0}
|
||||
buyInAmount={20}
|
||||
playerCount={6}
|
||||
buyInAmount={25}
|
||||
selectedCurrency={"$"}
|
||||
totalChipsCount={[]}
|
||||
totalChipsCount={[100, 50]}
|
||||
/>
|
||||
);
|
||||
expect(getByText("No valid distribution calculated yet.")).toBeTruthy();
|
||||
});
|
||||
const warning = getByText("TestIcon");
|
||||
expect(warning).toBeTruthy();
|
||||
|
||||
test.skip("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 as per actual component calculations
|
||||
|
||||
const { getByText } = render(
|
||||
<ChipDistributionSummary
|
||||
playerCount={playerCount}
|
||||
buyInAmount={100}
|
||||
totalChipsCount={totalChipsCount}
|
||||
selectedCurrency={"$"}
|
||||
/>
|
||||
jest.spyOn(Alert, "alert");
|
||||
fireEvent.press(warning);
|
||||
expect(Alert.alert).toHaveBeenCalledWith(
|
||||
"Warning",
|
||||
`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`
|
||||
);
|
||||
|
||||
expect(getByText("Distribution & Denomination")).toBeTruthy();
|
||||
|
||||
expectedDistribution.forEach((count, index) => {
|
||||
expect(getByText(new RegExp(`^${count}\\s+chips:`, "i"))).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,6 @@ import {
|
||||
userEvent,
|
||||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
fireEvent,
|
||||
} from "@testing-library/react-native";
|
||||
import ChipsSelector from "@/components/ChipsSelector";
|
||||
@ -55,7 +54,8 @@ describe("tests for ChipsSelector", () => {
|
||||
const green = screen.getByText("60");
|
||||
expect(green).toHaveStyle({ color: "green" });
|
||||
|
||||
userEvent.press(green);
|
||||
fireEvent.press(green);
|
||||
|
||||
const modalLabel = await screen.findByText(/number of green chips/i);
|
||||
expect(modalLabel).toBeDefined();
|
||||
|
||||
@ -80,27 +80,16 @@ describe("tests for ChipsSelector", () => {
|
||||
TOTAL_CHIPS_COUNT[4],
|
||||
]);
|
||||
});
|
||||
// skip: There is a jest/DOM issue with the button interaction, despite working correctly in-app. Documented to resolve.
|
||||
it.skip("test dec/inc buttons", async () => {
|
||||
rend();
|
||||
|
||||
const blue = screen.getByText(TOTAL_CHIPS_COUNT[3].toString());
|
||||
const black = screen.getByText(TOTAL_CHIPS_COUNT[4].toString());
|
||||
it("test dec/inc buttons", async () => {
|
||||
rend();
|
||||
const decrement = screen.getByRole("button", { name: /-/i });
|
||||
const increment = screen.getByRole("button", { name: /\+/i });
|
||||
|
||||
fireEvent.press(decrement);
|
||||
fireEvent.press(decrement);
|
||||
|
||||
// Test that elements are removed after fireEvent
|
||||
await waitForElementToBeRemoved(() => blue);
|
||||
await waitForElementToBeRemoved(() => black);
|
||||
expect(mockSetNumberOfChips).toHaveBeenCalledWith(4);
|
||||
|
||||
fireEvent.press(increment);
|
||||
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());
|
||||
expect(mockSetNumberOfChips).toHaveBeenCalledWith(4);
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,82 @@
|
||||
import { ButtonProps, Button } from "react-native";
|
||||
import { COLORS } from "@/styles/styles";
|
||||
import React, { useMemo } from "react";
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
useColorScheme,
|
||||
} from "react-native";
|
||||
|
||||
// More styling can be done, or swap this out with more flexible component like a TouchableOpacity if needed
|
||||
const AppButton = (props: ButtonProps) => (
|
||||
<Button color={COLORS.PRIMARY} {...props} />
|
||||
);
|
||||
interface ButtonProps {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
size?: "normal" | "small";
|
||||
}
|
||||
|
||||
export default AppButton;
|
||||
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;
|
||||
|
49
containers/Picker.tsx
Normal file
49
containers/Picker.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
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,7 +1,7 @@
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, useColorScheme } from "react-native";
|
||||
import React, { useMemo } from "react";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import globalStyles from "@/styles/styles";
|
||||
import globalStyles, { COLORS } from "@/styles/styles";
|
||||
|
||||
const titleCase = (input: string) =>
|
||||
input
|
||||
@ -9,18 +9,26 @@ const titleCase = (input: string) =>
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
|
||||
//Higher Order Component (HOC) for styling purposes
|
||||
// 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}>
|
||||
@ -28,11 +36,19 @@ const Section = ({
|
||||
style={styles.icon}
|
||||
name={iconName}
|
||||
size={30}
|
||||
color={"black"}
|
||||
color={colors.TEXT}
|
||||
/>
|
||||
<Text style={styles.title}>{titleCase(title)}</Text>
|
||||
<Text style={[styles.title, { color: colors.TEXT }]}>
|
||||
{titleCase(title)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ ...styles.content, flexDirection: orientation }}>
|
||||
<View
|
||||
style={{
|
||||
...styles.content,
|
||||
...contentStyle,
|
||||
flexDirection: orientation,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
|
54
i18n/en.json
Normal file
54
i18n/en.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"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
Normal file
55
i18n/es.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"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
Normal file
22
i18n/i18n.ts
Normal file
@ -0,0 +1,22 @@
|
||||
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;
|
182
package-lock.json
generated
182
package-lock.json
generated
@ -13,7 +13,7 @@
|
||||
"@react-native-picker/picker": "^2.11.0",
|
||||
"@react-navigation/bottom-tabs": "7.2.0",
|
||||
"@react-navigation/native": "7.0.14",
|
||||
"expo": "52.0.37",
|
||||
"expo": "^52.0.37",
|
||||
"expo-blur": "14.0.3",
|
||||
"expo-constants": "17.0.7",
|
||||
"expo-file-system": "18.0.11",
|
||||
@ -21,20 +21,22 @@
|
||||
"expo-haptics": "14.0.1",
|
||||
"expo-image-picker": "16.0.6",
|
||||
"expo-linking": "7.0.5",
|
||||
"expo-localization": "~16.0.1",
|
||||
"expo-router": "4.0.17",
|
||||
"expo-splash-screen": "0.29.22",
|
||||
"expo-status-bar": "2.0.1",
|
||||
"expo-symbols": "0.2.2",
|
||||
"expo-system-ui": "4.0.8",
|
||||
"expo-web-browser": "14.0.2",
|
||||
"i18next": "^24.2.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-native": "0.76.7",
|
||||
"react-native-gesture-handler": "2.20.2",
|
||||
"react-native-reanimated": "3.16.7",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "4.4.0",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"react-native-web": "0.19.13",
|
||||
"react-native-webview": "13.12.5"
|
||||
},
|
||||
@ -8238,6 +8240,19 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-localization": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-16.0.1.tgz",
|
||||
"integrity": "sha512-kUrXiV/Pq9r7cG+TMt+Qa49IUQ9Y/czVwen4hmiboTclTopcWdIeCzYZv6JGtufoPpjEO9vVx1QJrXYl9V2u0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rtl-detect": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-modules-autolinking": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.0.8.tgz",
|
||||
@ -9337,6 +9352,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@ -9406,6 +9430,37 @@
|
||||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "24.2.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz",
|
||||
"integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@ -14291,6 +14346,28 @@
|
||||
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz",
|
||||
"integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
@ -14446,92 +14523,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
|
||||
"integrity": "sha512-n5HGcxUuVaTf9QJPs/W22xQpC2Z9u0nb0KgLPnVltP8vdUvOp6+R26gF55kilP/fV4eL4vsAHUqUjewppJMBOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2",
|
||||
"yargs": "^16.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"fa-upgrade.sh": "bin/fa-upgrade.sh",
|
||||
"fa5-upgrade": "bin/fa5-upgrade.sh",
|
||||
"fa6-upgrade": "bin/fa6-upgrade.sh",
|
||||
"generate-icon": "bin/generate-icon.js"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.19.13",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
|
||||
@ -15091,6 +15082,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rtl-detect": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz",
|
||||
"integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@ -17220,6 +17217,15 @@
|
||||
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"@react-native-picker/picker": "^2.11.0",
|
||||
"@react-navigation/bottom-tabs": "7.2.0",
|
||||
"@react-navigation/native": "7.0.14",
|
||||
"expo": "52.0.37",
|
||||
"expo": "^52.0.37",
|
||||
"expo-blur": "14.0.3",
|
||||
"expo-constants": "17.0.7",
|
||||
"expo-file-system": "18.0.11",
|
||||
@ -31,14 +31,17 @@
|
||||
"expo-haptics": "14.0.1",
|
||||
"expo-image-picker": "16.0.6",
|
||||
"expo-linking": "7.0.5",
|
||||
"expo-localization": "~16.0.1",
|
||||
"expo-router": "4.0.17",
|
||||
"expo-splash-screen": "0.29.22",
|
||||
"expo-status-bar": "2.0.1",
|
||||
"expo-symbols": "0.2.2",
|
||||
"expo-system-ui": "4.0.8",
|
||||
"expo-web-browser": "14.0.2",
|
||||
"i18next": "^24.2.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-native": "0.76.7",
|
||||
"react-native-gesture-handler": "2.20.2",
|
||||
"react-native-reanimated": "3.16.7",
|
||||
|
@ -1,16 +1,23 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
export const COLORS = {
|
||||
PRIMARY: "#007bff",
|
||||
SECONDARY: "#6c757d",
|
||||
SUCCESS: "#28a745",
|
||||
DANGER: "#dc3545",
|
||||
WARNING: "#ffc107",
|
||||
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 lightStyles = StyleSheet.create({});
|
||||
const darkStyles = StyleSheet.create({});
|
||||
|
||||
const GlobalStyles = StyleSheet.create({
|
||||
scrollView: {},
|
||||
scrollViewContent: {
|
||||
@ -25,7 +32,7 @@ const GlobalStyles = StyleSheet.create({
|
||||
gap: 10,
|
||||
},
|
||||
|
||||
h1: { fontSize: 20, fontWeight: "bold" },
|
||||
h1: { fontSize: 19, fontWeight: "bold" },
|
||||
h2: { fontSize: 18, fontWeight: "normal" },
|
||||
p: {
|
||||
fontSize: 16,
|
||||
@ -51,9 +58,15 @@ const GlobalStyles = StyleSheet.create({
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
picker: {
|
||||
height: 50,
|
||||
borderRadius: 10,
|
||||
height: 55,
|
||||
width: 150,
|
||||
},
|
||||
pickerItem: {},
|
||||
pickerWrapper: {
|
||||
borderRadius: 10,
|
||||
overflow: "hidden",
|
||||
},
|
||||
});
|
||||
|
||||
export default GlobalStyles;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { saveState, loadState, PokerState } from "../CalculatorState";
|
||||
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.tsx", () => {
|
||||
describe("CalculatorState.ts", () => {
|
||||
const mockState: PokerState = {
|
||||
playerCount: 4,
|
||||
buyInAmount: 50,
|
@ -3,14 +3,14 @@ import {
|
||||
savePersistentState,
|
||||
loadPersistentState,
|
||||
PokerState,
|
||||
} from "../PersistentState";
|
||||
} from "@/util/PersistentState";
|
||||
|
||||
jest.mock("@react-native-async-storage/async-storage", () => ({
|
||||
setItem: jest.fn(),
|
||||
getItem: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("PersistentState.tsx", () => {
|
||||
describe("PersistentState.ts", () => {
|
||||
const mockState: PokerState = {
|
||||
playerCount: 4,
|
||||
buyInAmount: 50,
|
Loading…
Reference in New Issue
Block a user