Compare commits

...

13 Commits

Author SHA1 Message Date
David Westgate
2b7a9be089
Merge pull request #61 from djwesty/djwesty/60
Address skipped tests
2025-03-18 16:05:34 -07:00
David Westgate
b98d0035d8 remove unused imports 2025-03-18 15:23:25 -07:00
David Westgate
cf0b0332a4 remove uneeded test; fix other skipped tests 2025-03-18 15:21:06 -07:00
Mantasha Altab Noyela
e8898d8a60
Merge pull request #59 from djwesty/MantashaNoyela/22
Changed Styles and Screen Size for Mobile Device
2025-03-18 15:11:30 -07:00
David Westgate
2e5efda69a abstract picker component; added small buttons; visual tweaks 2025-03-17 23:48:31 -07:00
David Westgate
e720e2e010 global darkmode hook usage; use style abstractions; remove ui wrapper causing issues 2025-03-17 23:10:31 -07:00
MantashaNoyela
68a91a32ad Moved the dark mode to settings bar 2025-03-17 06:42:59 -07:00
MantashaNoyela
28addf53a5 Fixed issues 2025-03-16 16:08:07 -07:00
MantashaNoyela
533356117a Changed styles 2025-03-11 06:40:52 -07:00
Vutukuri15
da695be36d
Merge pull request #58 from djwesty/vutukuri15/57
Fixed issue with extra colors of image input # 57
2025-03-10 12:08:51 -07:00
vutukuri15
63ecde6c99 Fixed test code 2025-03-10 12:01:36 -07:00
vutukuri15
430750e3d4 Fixed issue with extra colors of image input 2025-03-09 20:54:47 -07:00
David Westgate
84a77ebb58
Merge pull request #56 from djwesty/djwesty/45
Bug Fix: Image Detection setting chip colors (Issue #45)
2025-03-09 20:19:42 -07:00
22 changed files with 347 additions and 208 deletions

View File

@ -1,9 +1,11 @@
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);
@ -17,20 +19,36 @@ 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}>
<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>

View File

@ -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,16 +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 { Picker } from "@react-native-picker/picker";
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);
@ -25,23 +26,25 @@ const IndexScreen: React.FC = () => {
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);
@ -62,41 +65,29 @@ const IndexScreen: React.FC = () => {
totalChipsCount,
selectedCurrency,
};
try {
await saveState(slot, state);
await savePersistentState(state);
console.log(`Game state saved to ${slot}:`, state);
Alert.alert(i18n.t("success"), i18n.t("state_saved", { slot })); // Fixed interpolation
} catch (error) {
console.error("Error saving state:", error);
Alert.alert(i18n.t("error"), i18n.t("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 ?? 20);
setNumberOfChips(loadedState.numberOfChips);
setTotalChipsCount(loadedState.totalChipsCount);
setSelectedCurrency(loadedState.selectedCurrency || "$");
await savePersistentState(loadedState);
console.log(`Game state loaded from ${slot}:`, loadedState);
Alert.alert(i18n.t("success"), i18n.t("state_loaded_from", { slot })); // Fixed interpolation
} else {
Alert.alert(i18n.t("info"), i18n.t("no_saved_state_found"));
}
} catch (error) {
console.error("Error loading state:", error);
Alert.alert(i18n.t("error"), i18n.t("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: string) => {
setSelectedLanguage(language);
i18n.changeLanguage(language);
const handleLanguageChange = (language: ItemValue, _: any) => {
setSelectedLanguage(language.toString());
i18n.changeLanguage(language.toString());
};
return (
@ -105,33 +96,49 @@ const IndexScreen: React.FC = () => {
contentContainerStyle={styles.scrollViewContent}
>
{isSettingsVisible && (
<Section
title={i18n.t("select_language")}
iconName={"language"}
orientation="row"
>
<Picker
selectedValue={selectedLanguage}
onValueChange={handleLanguageChange}
style={styles.picker}
<>
<Section
title={i18n.t("appearance")}
iconName={"brightness-4"}
orientation="row"
>
<Picker.Item label={i18n.t("english")} value="en" />
<Picker.Item label={i18n.t("spanish")} value="es" />
</Picker>
</Section>
)}
<Button
title={
darkMode
? i18n.t("switch_to_light_mode")
: i18n.t("switch_to_dark_mode")
}
onPress={() =>
Appearance.setColorScheme(darkMode ? "light" : "dark")
}
/>
</Section>
{isSettingsVisible && (
<Section
title={i18n.t("select_currency")}
iconName={"attach-money"}
orientation="row"
>
<CurrencySelector
selectedCurrency={selectedCurrency}
setSelectedCurrency={setSelectedCurrency}
/>
</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
@ -205,19 +212,23 @@ const IndexScreen: React.FC = () => {
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>

View File

@ -1,5 +1,5 @@
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";
@ -25,6 +25,12 @@ 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 = parseRoundClamp(value);
@ -51,7 +57,6 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({
{defaultBuyInOptions.map((amount) => (
<Button
key={amount}
color={buyInAmount === amount ? COLORS.PRIMARY : COLORS.SECONDARY}
onPress={() => handleBuyInSelection(amount)}
title={`${selectedCurrency} ${amount}`}
/>
@ -59,7 +64,8 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({
</View>
<TextInput
style={styles.input}
style={[styles.input, { color: colors.TEXT }]}
placeholderTextColor={colors.TEXT}
value={customAmount}
maxLength={3}
onChangeText={handleCustomAmountChange}
@ -67,7 +73,7 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({
keyboardType="numeric"
/>
<Text style={styles.h2}>
<Text style={[styles.h2, { color: colors.TEXT }]}>
{`${i18n.t("selected_buy_in")} `}
{buyInAmount !== null
? `${selectedCurrency} ${buyInAmount}`

View File

@ -16,6 +16,8 @@ const ChipDetection = ({
Record<string, number>
>({});
const chipColors = ["white", "red", "green", "blue", "black"];
const requestCameraPermissions = async () => {
const cameraPermission = await ImagePicker.requestCameraPermissionsAsync();
return cameraPermission.granted;
@ -92,16 +94,26 @@ const ChipDetection = ({
const result = await response.json();
if (!response.ok || !result.choices || !result.choices[0].message) {
throw new Error(i18n.t("invalid_response")); // Translate invalid response error
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 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);

View File

@ -32,7 +32,6 @@ const ChipInputModal = ({
const [value, setValue] = useState<number | undefined>();
// Reset the color value when the specific color this modal is for changes
useEffect(() => {
setValue(totalChipsCount[colorIdx]);
}, [colorIdx, totalChipsCount]);
@ -129,11 +128,10 @@ const ChipsSelector = ({
]);
const colorsUsed = useMemo(
() => colors.slice(0, numberOfChips), // Only show as many colors as the `numberOfChips`
() => colors.slice(0, numberOfChips),
[numberOfChips]
);
// Callback for ChipInputModal to update the chips in the parent's state.
const update = useCallback(
(color: ColorValue, count: number) => {
const newTotalChipsCount = totalChipsCount.slice();
@ -144,7 +142,6 @@ const ChipsSelector = ({
[totalChipsCount, setTotalChipsCount]
);
// Handling number of chips to make sure the array updates accordingly
useEffect(() => {
if (numberOfChips !== totalChipsCount.length) {
let newTotalChipsCount = totalChipsCount.slice();
@ -169,7 +166,7 @@ const ChipsSelector = ({
onPress={() => {
setNumberOfChips(Math.max(1, numberOfChips - 1));
}}
disabled={numberOfChips == 1}
disabled={numberOfChips === 1}
/>
<View style={[styles.container, { flexDirection: "row" }]}>
{colorsUsed.map((color) => {
@ -189,7 +186,7 @@ const ChipsSelector = ({
onPress={() => {
setNumberOfChips(Math.min(5, numberOfChips + 1));
}}
disabled={numberOfChips == 5}
disabled={numberOfChips === 5}
/>
<ChipInputModal
@ -202,35 +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;

View File

@ -1,7 +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;
@ -16,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={i18n.t("usd")} value="$" />
<Picker.Item label={i18n.t("euro")} value="€" />
<Picker.Item label={i18n.t("pound")} value="£" />
<Picker.Item label={i18n.t("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>
</>
);

View File

@ -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,21 +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 (
<>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<Button
title="-"
onPress={decreasePlayers}
disabled={playerCount <= MIN}
/>
<Text style={styles.h1}>{playerCount}</Text>
<Text style={[styles.h1, { color: colors.TEXT }]}>{playerCount}</Text>
<Button
title="+"
onPress={increasePlayers}
disabled={playerCount >= MAX}
/>
</>
</View>
);
};

View File

@ -40,7 +40,7 @@ describe("ChipDetection", () => {
});
afterEach(() => {
jest.restoreAllMocks(); // Reset all mocks to prevent test contamination
jest.restoreAllMocks();
});
it("renders correctly", () => {
@ -83,7 +83,11 @@ describe("ChipDetection", () => {
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,
})
);
});
@ -144,7 +148,7 @@ describe("ChipDetection", () => {
choices: [
{
message: {
content: JSON.stringify({ red: 5, green: 3 }),
content: JSON.stringify({ red: 5, green: 3, blue: 0 }),
},
},
],
@ -163,6 +167,7 @@ describe("ChipDetection", () => {
expect(mockUpdateChipCount).toHaveBeenCalledWith({
red: 5,
green: 3,
blue: 0,
})
);
});

View File

@ -1,5 +1,7 @@
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", () => {
@ -35,46 +37,23 @@ describe("ChipDistributionSummary Component", () => {
});
});
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) => {
expect(getByText(new RegExp(`^${count}\\s+chips:`, "i"))).toBeTruthy();
});
});
});

View File

@ -3,9 +3,7 @@ import {
userEvent,
render,
screen,
waitForElementToBeRemoved,
fireEvent,
act,
} from "@testing-library/react-native";
import ChipsSelector from "@/components/ChipsSelector";
@ -82,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);
});
});

View File

@ -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
View 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>
);
};

View File

@ -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
@ -23,6 +23,12 @@ const Section = ({
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}>
@ -30,9 +36,11 @@ 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={{

View File

@ -46,6 +46,9 @@
"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"
"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"
}
}

View File

@ -47,6 +47,9 @@
"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."
"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"
}
}

2
package-lock.json generated
View File

@ -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",

View File

@ -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,6 +31,7 @@
"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",
@ -47,8 +48,7 @@
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.4.0",
"react-native-web": "0.19.13",
"react-native-webview": "13.12.5",
"expo-localization": "~16.0.1"
"react-native-webview": "13.12.5"
},
"devDependencies": {
"@babel/core": "7.26.9",

View File

@ -1,16 +1,23 @@
import { StyleSheet } from "react-native";
export const COLORS = {
PRIMARY: "#007bff",
SECONDARY: "#6c757d",
SUCCESS: "#28a745",
DANGER: "#dc3545",
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;

View File

@ -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,

View File

@ -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,