Merge pull request #25 from djwesty/djwesty/7

Implemented Automatic Detection of chip counts and colors # 7
This commit is contained in:
Vutukuri15 2025-02-21 21:44:37 -08:00 committed by GitHub
commit 6f10efaac0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 619 additions and 70 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
API_KEY=Put Open AI key here
GPT_MODEL=gpt-4o-mini
#GPT_MODEL=gpt-4-turbo # More expensive model, use sparingly

2
.gitignore vendored
View File

@ -36,3 +36,5 @@ yarn-error.*
*.tsbuildinfo
app-example
android
.env

View File

@ -14,6 +14,23 @@ This applications uses the React Native + Expo framework and by extension is pri
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
### Setting Up Environment Variables
To set up your environment variables:
1. Copy the example environment variable file to create your own .env file:
cp .env.example .env
2. Open the .env file and add your OpenAI API key:
API_KEY=your_openai_api_key_here
MODEL_NAME=your_model_name
3. Save the .env file.
This setup allows you to run the application with your own API credentials, and you can switch models if needed.
### VSCode plugins
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)

View File

@ -15,7 +15,8 @@
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"package": "com.anonymous.pokerchipshelper"
},
"web": {
"bundler": "metro",

View File

@ -3,12 +3,15 @@ import { ScrollView, Text, Alert, Button } from "react-native";
import PlayerSelector from "@/components/PlayerSelector";
import BuyInSelector from "@/components/BuyInSelector";
import ChipsSelector from "@/components/ChipsSelector";
import ChipDistributionSummary from "@/components/ChipDistributionSummary";
import ChipDistributionSummary from "@/components/ChipDistributionSummary";
import ChipDetection from "@/components/ChipDetection";
const IndexScreen = () => {
const [playerCount, setPlayerCount] = useState(2);
const [buyInAmount, setBuyInAmount] = useState<number | null>(null);
const [numberOfChips, setNumberOfChips] = useState<number>(5);
const [totalChipsCount, setTotalChipsCount] = useState<number[]>([]);
const handleSave = () => {
if (buyInAmount === null) {
Alert.alert("Error", "Please select a valid buy-in amount");
@ -19,6 +22,16 @@ const IndexScreen = () => {
);
}
};
// Update chip count based on detection or manual edit
const updateChipCount = (chipData: { [color: string]: number }) => {
// Convert the chip data from the API response or manual edit to a count array
const chipCountArray = Object.entries(chipData).map(
([color, count]) => count
);
setTotalChipsCount(chipCountArray); // Update the parent component's state
};
return (
<ScrollView contentContainerStyle={{ padding: 20, flexGrow: 1 }}>
<Text style={{ fontSize: 24, marginBottom: 30, marginTop: 50 }}>
@ -29,6 +42,7 @@ const IndexScreen = () => {
setPlayerCount={setPlayerCount}
/>
<BuyInSelector setBuyInAmount={setBuyInAmount} />
<ChipDetection updateChipCount={updateChipCount} />
<ChipsSelector
totalChipsCount={totalChipsCount}
setTotalChipsCount={setTotalChipsCount}
@ -48,4 +62,5 @@ const IndexScreen = () => {
</ScrollView>
);
};
export default IndexScreen;

15
babel.config.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
presets: ["babel-preset-expo", "@babel/preset-typescript"],
plugins: [
[
"module:react-native-dotenv",
{
moduleName: "@env",
path: ".env",
safe: true,
allowUndefined: false,
},
],
"react-native-reanimated/plugin",
],
};

View File

@ -0,0 +1,136 @@
import React, { useState } from "react";
import {
View,
Button,
Image,
ActivityIndicator,
Text,
ScrollView,
} from "react-native";
import * as ImagePicker from "expo-image-picker";
import { API_KEY, MODEL_NAME } from "@env";
const ChipDetection = ({ updateChipCount }) => {
const [imageUri, setImageUri] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [lastDetectedChips, setLastDetectedChips] = useState({});
const requestCameraPermissions = async () => {
const cameraPermission = await ImagePicker.requestCameraPermissionsAsync();
return cameraPermission.granted;
};
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
base64: true,
quality: 1,
});
if (!result.canceled) {
setImageUri(result.assets[0].uri);
await processImage(result.assets[0].base64);
}
};
const takePhoto = async () => {
const hasPermission = await requestCameraPermissions();
if (!hasPermission) {
setError("Camera permission is required to take a photo.");
return;
}
const result = await ImagePicker.launchCameraAsync({
base64: true,
quality: 1,
});
if (!result.canceled) {
setImageUri(result.assets[0].uri);
await processImage(result.assets[0].base64);
}
};
const processImage = async (base64Image) => {
setLoading(true);
setError(null);
try {
const response = await fetch(
"https://api.openai.com/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: MODEL_NAME,
messages: [
{
role: "system",
content:
"Identify and count poker chips by color. Return only the count for each color in JSON format.",
},
{
role: "user",
content: [
{
type: "text",
text: "How many poker chips are there for each color? Return structured JSON.",
},
{
type: "image_url",
image_url: { url: `data:image/png;base64,${base64Image}` },
},
],
},
],
max_tokens: 1000,
}),
}
);
const result = await response.json();
if (!response.ok || !result.choices || !result.choices[0].message) {
throw new Error("Invalid response from API.");
}
const rawContent = result.choices[0].message.content.trim();
const cleanJSON = rawContent.replace(/```json|```/g, "").trim();
const parsedData = JSON.parse(cleanJSON);
// Filter out colors with a count of 0
const filteredData = Object.fromEntries(
Object.entries(parsedData).filter(([_, count]) => count > 0)
);
setLastDetectedChips(filteredData); // Store detected chip counts
updateChipCount(filteredData);
} catch (error) {
setError("Failed to analyze the image.");
} finally {
setLoading(false);
}
};
return (
<ScrollView contentContainerStyle={{ padding: 20, alignItems: "center" }}>
<Button title="Pick an Image" onPress={pickImage} />
<Button title="Take a Photo" onPress={takePhoto} />
{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>}
</ScrollView>
);
};
export default ChipDetection;

View File

@ -9,7 +9,7 @@ interface ChipDistributionSummaryProps {
colors?: ColorValue[];
}
const MAX_CHIPS = 500;
const MAX_CHIPS = 500;
const ChipDistributionSummary = ({
playerCount,
@ -22,16 +22,18 @@ const ChipDistributionSummary = ({
useEffect(() => {
if (buyInAmount !== null && playerCount > 0) {
let totalChips = totalChipsCount.reduce((sum, count) => sum + count, 0);
if (totalChips > MAX_CHIPS) {
const scaleFactor = MAX_CHIPS / totalChips;
totalChipsCount = totalChipsCount.map((count) => Math.floor(count * scaleFactor));
totalChipsCount = totalChipsCount.map((count) =>
Math.floor(count * scaleFactor)
);
totalChips = MAX_CHIPS;
}
const distribution = totalChipsCount.map((chipCount) =>
Math.floor(chipCount / playerCount)
);
setChipDistribution(distribution);
} else {
setChipDistribution([]);
@ -49,12 +51,14 @@ const ChipDistributionSummary = ({
<Text style={styles.title}>Chip Distribution Summary:</Text>
{hasValidDistribution ? (
chipDistribution.map((count, index) => (
<Text key={index} style={[styles.chipText, { color: colors[index] }]}>
{`${colors[index]?.toString().toUpperCase()} Chips: ${count} per player`}
<Text key={index} style={[styles.chipText, { color: colors[index] }]}>
{`${colors[index]?.toString().toUpperCase()} Chips: ${count} per player`}
</Text>
))
) : (
<Text style={styles.noDataText}>No valid distribution calculated yet.</Text>
<Text style={styles.noDataText}>
No valid distribution calculated yet.
</Text>
)}
</View>
);
@ -82,4 +86,4 @@ const styles = StyleSheet.create({
},
});
export default ChipDistributionSummary;
export default ChipDistributionSummary;

View File

@ -0,0 +1,150 @@
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react-native";
import ChipDetection from "@/components/ChipDetection";
import * as ImagePicker from "expo-image-picker";
const mockUpdateChipCount = jest.fn();
jest.mock("expo-image-picker", () => ({
requestCameraPermissionsAsync: jest.fn(),
launchImageLibraryAsync: jest.fn(),
launchCameraAsync: jest.fn(),
MediaTypeOptions: {
Images: "image",
},
}));
describe("ChipDetection", () => {
beforeEach(() => {
jest.clearAllMocks();
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
choices: [
{
message: {
content: JSON.stringify({ red: 5, green: 3, blue: 0 }),
},
},
],
}),
})
);
});
it("renders correctly", () => {
const { getByText } = render(
<ChipDetection updateChipCount={mockUpdateChipCount} />
);
expect(getByText("Pick an Image")).toBeTruthy();
expect(getByText("Take a Photo")).toBeTruthy();
});
it("picks an image from the library", async () => {
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
canceled: false,
assets: [{ uri: "test-uri", base64: "test-base64" }],
});
const { getByText } = render(
<ChipDetection updateChipCount={mockUpdateChipCount} />
);
fireEvent.press(getByText("Pick an Image"));
await waitFor(() => expect(mockUpdateChipCount).toHaveBeenCalled());
});
it("takes a photo with the camera", async () => {
ImagePicker.requestCameraPermissionsAsync.mockResolvedValueOnce({
granted: true,
});
ImagePicker.launchCameraAsync.mockResolvedValueOnce({
canceled: false,
assets: [{ uri: "test-camera-uri", base64: "test-camera-base64" }],
});
const { getByText } = render(
<ChipDetection updateChipCount={mockUpdateChipCount} />
);
fireEvent.press(getByText("Take a Photo"));
await waitFor(() => expect(mockUpdateChipCount).toHaveBeenCalled());
});
it("handles camera permission denied", async () => {
ImagePicker.requestCameraPermissionsAsync.mockResolvedValueOnce({
granted: false,
});
const { getByText } = render(
<ChipDetection updateChipCount={mockUpdateChipCount} />
);
fireEvent.press(getByText("Take a Photo"));
await waitFor(() =>
expect(
getByText("Camera permission is required to take a photo.")
).toBeTruthy()
);
});
it("displays error message on image processing failure", async () => {
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
canceled: false,
assets: [{ uri: "test-uri", base64: "test-base64" }],
});
global.fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: false,
json: () => Promise.resolve({ choices: [] }),
})
);
const { getByText } = render(
<ChipDetection updateChipCount={mockUpdateChipCount} />
);
fireEvent.press(getByText("Pick an Image"));
await waitFor(() =>
expect(getByText("Failed to analyze the image.")).toBeTruthy()
);
});
it("handles valid API response correctly", async () => {
ImagePicker.launchImageLibraryAsync.mockResolvedValueOnce({
canceled: false,
assets: [{ uri: "test-uri", base64: "test-base64" }],
});
global.fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
choices: [
{
message: {
content: JSON.stringify({ red: 5, green: 3 }),
},
},
],
}),
})
);
const { getByText } = render(
<ChipDetection updateChipCount={mockUpdateChipCount} />
);
fireEvent.press(getByText("Pick an Image"));
await waitFor(() =>
expect(mockUpdateChipCount).toHaveBeenCalledWith({
red: 5,
green: 3,
})
);
});
});

296
package-lock.json generated
View File

@ -8,14 +8,17 @@
"name": "poker-chips-helper",
"version": "1.0.0",
"dependencies": {
"@babel/preset-typescript": "^7.26.0",
"@expo/vector-icons": "^14.0.2",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"expo": "~52.0.31",
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.5",
"expo-file-system": "~18.0.11",
"expo-font": "~13.0.3",
"expo-haptics": "~14.0.1",
"expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5",
"expo-router": "~4.0.17",
"expo-splash-screen": "~0.29.21",
@ -23,9 +26,11 @@
"expo-symbols": "~0.2.2",
"expo-system-ui": "~4.0.8",
"expo-web-browser": "~14.0.2",
"metro-react-native-babel-preset": "^0.77.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react-native": "^0.76.7",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
@ -34,7 +39,7 @@
"react-native-webview": "13.12.5"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/core": "^7.26.9",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react": "^16.2.0",
@ -47,11 +52,12 @@
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-native": "^5.0.0",
"jest": "^29.2.1",
"jest": "^29.7.0",
"jest-expo": "~52.0.3",
"jest-fetch-mock": "^3.0.3",
"prettier": "^3.4.2",
"react-test-renderer": "18.3.1",
"typescript": "^5.3.3"
"typescript": "^5.7.3"
}
},
"node_modules/@0no-co/graphql.web": {
@ -105,21 +111,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz",
"integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
"integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.5",
"@babel/generator": "^7.26.9",
"@babel/helper-compilation-targets": "^7.26.5",
"@babel/helper-module-transforms": "^7.26.0",
"@babel/helpers": "^7.26.7",
"@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
"@babel/traverse": "^7.26.7",
"@babel/types": "^7.26.7",
"@babel/helpers": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/template": "^7.26.9",
"@babel/traverse": "^7.26.9",
"@babel/types": "^7.26.9",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@ -135,13 +141,13 @@
}
},
"node_modules/@babel/generator": {
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
"integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.26.5",
"@babel/types": "^7.26.5",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@ -232,6 +238,18 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz",
"integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
@ -385,13 +403,13 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.7"
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
@ -484,12 +502,12 @@
}
},
"node_modules/@babel/parser": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.7"
"@babel/types": "^7.26.9"
},
"bin": {
"parser": "bin/babel-parser.js"
@ -582,6 +600,25 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/plugin-proposal-async-generator-functions": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz",
"integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==",
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.",
"license": "MIT",
"dependencies": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-plugin-utils": "^7.20.2",
"@babel/helper-remap-async-to-generator": "^7.18.9",
"@babel/plugin-syntax-async-generators": "^7.8.4"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-class-properties": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz",
@ -648,6 +685,60 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-numeric-separator": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz",
"integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==",
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.18.6",
"@babel/plugin-syntax-numeric-separator": "^7.10.4"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-object-rest-spread": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz",
"integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==",
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.",
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.20.5",
"@babel/helper-compilation-targets": "^7.20.7",
"@babel/helper-plugin-utils": "^7.20.2",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3",
"@babel/plugin-transform-parameters": "^7.20.7"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-optional-catch-binding": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz",
"integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==",
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.18.6",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-optional-chaining": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
@ -2158,30 +2249,30 @@
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
"@babel/parser": "^7.25.9",
"@babel/types": "^7.25.9"
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
"integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.5",
"@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
"@babel/types": "^7.26.7",
"@babel/generator": "^7.26.9",
"@babel/parser": "^7.26.9",
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.9",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@ -2209,9 +2300,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@ -8075,9 +8166,9 @@
}
},
"node_modules/expo-file-system": {
"version": "18.0.10",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.10.tgz",
"integrity": "sha512-+GnxkI+J9tOzUQMx+uIOLBEBsO2meyoYHxd87m9oT9M//BpepYqI1AvYBH8YM4dgr9HaeaeLr7z5XFVqfL8tWg==",
"version": "18.0.11",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.11.tgz",
"integrity": "sha512-yDwYfEzWgPXsBZHJW2RJ8Q66ceiFN9Wa5D20pp3fjXVkzPBDwxnYwiPWk4pVmCa5g4X5KYMoMne1pUrsL4OEpg==",
"license": "MIT",
"dependencies": {
"web-streams-polyfill": "^3.3.2"
@ -8109,6 +8200,27 @@
"expo": "*"
}
},
"node_modules/expo-image-loader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.0.0.tgz",
"integrity": "sha512-Eg+5FHtyzv3Jjw9dHwu2pWy4xjf8fu3V0Asyy42kO+t/FbvW/vjUixpTjPtgKQLQh+2/9Nk4JjFDV6FwCnF2ZA==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image-picker": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.0.6.tgz",
"integrity": "sha512-HN4xZirFjsFDIsWFb12AZh19fRzuvZjj2ll17cGr19VNRP06S/VPQU3Tdccn5vwUzQhOBlLu704CnNm278boiQ==",
"license": "MIT",
"dependencies": {
"expo-image-loader": "~5.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-keep-awake": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.2.tgz",
@ -10587,6 +10699,17 @@
"license": "MIT",
"peer": true
},
"node_modules/jest-fetch-mock": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
"integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-fetch": "^3.0.4",
"promise-polyfill": "^8.1.3"
}
},
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
@ -12096,6 +12219,68 @@
"node": ">=18.18"
}
},
"node_modules/metro-react-native-babel-preset": {
"version": "0.77.0",
"resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.77.0.tgz",
"integrity": "sha512-HPPD+bTxADtoE4y/4t1txgTQ1LVR6imOBy7RMHUsqMVTbekoi8Ph5YI9vKX2VMPtVWeFt0w9YnCSLPa76GcXsA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.0",
"@babel/plugin-proposal-async-generator-functions": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.18.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0",
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.20.0",
"@babel/plugin-proposal-optional-catch-binding": "^7.0.0",
"@babel/plugin-proposal-optional-chaining": "^7.20.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.0",
"@babel/plugin-syntax-export-default-from": "^7.0.0",
"@babel/plugin-syntax-flow": "^7.18.0",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-syntax-optional-chaining": "^7.0.0",
"@babel/plugin-transform-arrow-functions": "^7.0.0",
"@babel/plugin-transform-async-to-generator": "^7.20.0",
"@babel/plugin-transform-block-scoping": "^7.0.0",
"@babel/plugin-transform-classes": "^7.0.0",
"@babel/plugin-transform-computed-properties": "^7.0.0",
"@babel/plugin-transform-destructuring": "^7.20.0",
"@babel/plugin-transform-flow-strip-types": "^7.20.0",
"@babel/plugin-transform-function-name": "^7.0.0",
"@babel/plugin-transform-literals": "^7.0.0",
"@babel/plugin-transform-modules-commonjs": "^7.0.0",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0",
"@babel/plugin-transform-parameters": "^7.0.0",
"@babel/plugin-transform-react-display-name": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/plugin-transform-react-jsx-self": "^7.0.0",
"@babel/plugin-transform-react-jsx-source": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/plugin-transform-shorthand-properties": "^7.0.0",
"@babel/plugin-transform-spread": "^7.0.0",
"@babel/plugin-transform-sticky-regex": "^7.0.0",
"@babel/plugin-transform-typescript": "^7.5.0",
"@babel/plugin-transform-unicode-regex": "^7.0.0",
"@babel/template": "^7.0.0",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.4.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@babel/core": "*"
}
},
"node_modules/metro-react-native-babel-preset/node_modules/react-refresh": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
"integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/metro-resolver": {
"version": "0.81.1",
"resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.81.1.tgz",
@ -13573,6 +13758,13 @@
"asap": "~2.0.3"
}
},
"node_modules/promise-polyfill": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
"dev": true,
"license": "MIT"
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -13916,6 +14108,18 @@
}
}
},
"node_modules/react-native-dotenv": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz",
"integrity": "sha512-6vnIE+WHABSeHCaYP6l3O1BOEhWxKH6nHAdV7n/wKn/sciZ64zPPp2NUdEUf1m7g4uuzlLbjgr+6uDt89q2DOg==",
"license": "MIT",
"dependencies": {
"dotenv": "^16.4.5"
},
"peerDependencies": {
"@babel/runtime": "^7.20.6"
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.20.2",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",

View File

@ -15,14 +15,17 @@
"preset": "jest-expo"
},
"dependencies": {
"@babel/preset-typescript": "^7.26.0",
"@expo/vector-icons": "^14.0.2",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"expo": "~52.0.31",
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.5",
"expo-file-system": "~18.0.11",
"expo-font": "~13.0.3",
"expo-haptics": "~14.0.1",
"expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5",
"expo-router": "~4.0.17",
"expo-splash-screen": "~0.29.21",
@ -30,9 +33,11 @@
"expo-symbols": "~0.2.2",
"expo-system-ui": "~4.0.8",
"expo-web-browser": "~14.0.2",
"metro-react-native-babel-preset": "^0.77.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react-native": "^0.76.7",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
@ -41,7 +46,7 @@
"react-native-webview": "13.12.5"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/core": "^7.26.9",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react": "^16.2.0",
@ -54,11 +59,12 @@
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-native": "^5.0.0",
"jest": "^29.2.1",
"jest": "^29.7.0",
"jest-expo": "~52.0.3",
"jest-fetch-mock": "^3.0.3",
"prettier": "^3.4.2",
"react-test-renderer": "18.3.1",
"typescript": "^5.3.3"
"typescript": "^5.7.3"
},
"private": true
}

View File

@ -1,18 +1,14 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"jsx": "react",
"jsx": "react-native", // Update this to "react-native" for React Native compatibility
"strict": true,
"esModuleInterop": true, // Ensures compatibility with modules
"skipLibCheck": true, // Skips type checking of declaration files for faster builds
"moduleResolution": "node", // Ensures modules are resolved correctly
"paths": {
"@/*": [
"./*"
]
"@/*": ["./*"]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}