From dac208a3970abe5c34dada7acf163eb322d5a966 Mon Sep 17 00:00:00 2001 From: Sosokker Date: Sun, 11 May 2025 02:46:23 +0700 Subject: [PATCH] feat: add image upload, camera --- app.config.js | 8 +- app/(tabs)/home.tsx | 357 +++++++++++++++++------------------ package-lock.json | 319 +++++++++++++++++++++++++------ package.json | 8 + services/data/foods.ts | 91 ++++++++- services/data/imageUpload.ts | 36 ++++ services/gemini.ts | 10 + services/supabase.ts | 17 +- types.ts | 29 ++- 9 files changed, 623 insertions(+), 252 deletions(-) create mode 100644 services/data/imageUpload.ts create mode 100644 services/gemini.ts diff --git a/app.config.js b/app.config.js index 5e34875..1d846e2 100644 --- a/app.config.js +++ b/app.config.js @@ -14,6 +14,7 @@ export default { supportsTablet: true }, android: { + usesCleartextTraffic: true, adaptiveIcon: { foregroundImage: './assets/images/adaptive-icon.png', backgroundColor: '#ffffff' @@ -42,12 +43,7 @@ export default { typedRoutes: true }, extra: { - FIREBASE_API_KEY: process.env.FIREBASE_API_KEY, - FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN, - FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID, - FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET, - FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID, - FIREBASE_APP_ID: process.env.FIREBASE_APP_ID, + GEMINI_API_KEY: process.env.GEMINI_API_KEY, }, }, }; diff --git a/app/(tabs)/home.tsx b/app/(tabs)/home.tsx index f587fb9..e9d9502 100644 --- a/app/(tabs)/home.tsx +++ b/app/(tabs)/home.tsx @@ -1,8 +1,14 @@ import { IconSymbol } from "@/components/ui/IconSymbol"; +import { getFoods, insertGenAIResult } from "@/services/data/foods"; +import { uploadImageToSupabase } from "@/services/data/imageUpload"; +import { callGenAIonImage } from "@/services/gemini"; +import { supabase } from "@/services/supabase"; import { Feather, FontAwesome, Ionicons } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; +import * as FileSystem from "expo-file-system"; import * as ImagePicker from "expo-image-picker"; import { router } from "expo-router"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { Alert, Image, @@ -15,148 +21,96 @@ import { View, } from "react-native"; -// Sample recipe data -const foodHighlights = [ - { - id: 1, - name: "Pad Kra Pao Moo Sab with Eggs", - image: require("@/assets/images/food/padkrapao.jpg"), - description: "Thai stir-fry with ground pork and holy basil", - time: "30 Mins", - calories: "520 kcal", - }, - { - id: 2, - name: "Jjajangmyeon", - image: require("@/assets/images/food/jjajangmyeon.jpg"), - description: "Korean black bean noodles", - time: "45 Mins", - calories: "650 kcal", - }, - { - id: 3, - name: "Ramen", - image: require("@/assets/images/food/ramen.jpg"), - description: "Japanese noodle soup", - time: "60 Mins", - calories: "480 kcal", - }, - { - id: 4, - name: "Beef Wellington", - image: require("@/assets/images/food/beef.jpg"), - description: "Tender beef wrapped in puff pastry", - time: "90 Mins", - calories: "750 kcal", - }, -]; +const useFoodsQuery = () => { + return useQuery({ + queryKey: ["highlight-foods"], + queryFn: async () => { + const { data, error } = await getFoods(undefined, true, undefined, 4); + if (error) throw error; + return data || []; + }, + staleTime: 1000 * 60 * 5, + }); +}; + +const runImagePipeline = async ( + imageBase64: string, + imageType: string, + userId: string +) => { + const imageUri = await uploadImageToSupabase(imageBase64, imageType, userId); + const genAIResult = await callGenAIonImage(imageUri); + if (genAIResult.error) throw genAIResult.error; + const { data: genAIResultData } = genAIResult; + if (!genAIResultData) throw new Error("GenAI result is null"); + await insertGenAIResult(genAIResultData, userId, imageUri); +}; + +const processImage = async ( + asset: ImagePicker.ImagePickerAsset, + userId: string +) => { + const base64 = await FileSystem.readAsStringAsync(asset.uri, { + encoding: "base64", + }); + const imageType = asset.mimeType || "image/jpeg"; + await runImagePipeline(base64, imageType, userId); +}; const navigateToFoodDetail = (foodId: string) => { router.push({ pathname: "/recipe-detail", params: { id: foodId } }); }; -export default function HomeScreen() { - const [searchQuery, setSearchQuery] = useState(""); - const [filteredRecipes, setFilteredRecipes] = useState(foodHighlights); +const handleImageSelection = async ( + pickerFn: + | typeof ImagePicker.launchCameraAsync + | typeof ImagePicker.launchImageLibraryAsync +) => { + const result = await pickerFn({ + mediaTypes: ["images"], + allowsEditing: true, + aspect: [1, 1], + quality: 1, + }); - // Handle search - const handleSearch = (text: string): void => { - setSearchQuery(text); - if (text) { - const filtered = foodHighlights.filter((food) => - food.name.toLowerCase().includes(text.toLowerCase()) - ); - setFilteredRecipes(filtered); - } else { - setFilteredRecipes(foodHighlights); - } - }; - - // Handle camera - const takePhoto = async () => { - const { status } = await ImagePicker.requestCameraPermissionsAsync(); - - if (status !== "granted") { + if (!result.canceled) { + try { + const { data, error } = await supabase.auth.getUser(); + if (error || !data?.user?.id) throw new Error("Cannot get user id"); + const userId = data.user.id; + await processImage(result.assets[0], userId); + } catch (err) { Alert.alert( - "Permission needed", - "Please grant camera permissions to use this feature." + "Image Processing Failed", + (err as Error).message || "Unknown error" ); - return; } - - const result = await ImagePicker.launchCameraAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [1, 1], - quality: 1, - }); - - if (!result.canceled) { - // Navigate to recipe detail with the captured image - router.push({ - pathname: "/recipe-detail", - params: { - title: "My New Recipe", - image: result.assets[0].uri, - }, - }); - } - }; - - // Handle gallery - const pickImage = async () => { - const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); - - if (status !== "granted") { - Alert.alert( - "Permission needed", - "Please grant media library permissions to use this feature." - ); - return; - } - - const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [1, 1], - quality: 1, - }); - - if (!result.canceled) { - // Navigate to recipe detail with the selected image - router.push({ - pathname: "/recipe-detail", - params: { - title: "My New Recipe", - image: result.assets[0].uri, - }, - }); - } - }; - - // Navigate to recipe detail - interface Recipe { - id: number; - title: string; - image: string; - color: string; - } - - const goToRecipeDetail = (recipe: Recipe): void => { router.push({ pathname: "/recipe-detail", params: { - title: recipe.title, - image: recipe.image, + title: "My New Recipe", + image: result.assets[0].uri, }, }); - }; + } +}; + +export default function HomeScreen() { + const [searchQuery, setSearchQuery] = useState(""); + const { data: foodsData = [], isLoading, error } = useFoodsQuery(); + + const filteredFoods = useMemo(() => { + return searchQuery + ? foodsData.filter((food) => + food.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : foodsData; + }, [foodsData, searchQuery]); return ( - {/* Header - Fixed at top */} Hi! Mr. Chef @@ -164,13 +118,11 @@ export default function HomeScreen() { - {/* Scrollable Content */} - {/* Show your dishes */} Show your dishes @@ -182,7 +134,7 @@ export default function HomeScreen() { className="flex-1" placeholder="Search..." value={searchQuery} - onChangeText={handleSearch} + onChangeText={setSearchQuery} /> @@ -192,7 +144,18 @@ export default function HomeScreen() { { + const { status } = + await ImagePicker.requestCameraPermissionsAsync(); + if (status !== "granted") { + Alert.alert( + "Permission needed", + "Please grant camera permissions." + ); + return; + } + await handleImageSelection(ImagePicker.launchCameraAsync); + }} > @@ -202,10 +165,20 @@ export default function HomeScreen() { - { + const { status } = + await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (status !== "granted") { + Alert.alert( + "Permission needed", + "Please grant gallery permissions." + ); + return; + } + await handleImageSelection(ImagePicker.launchImageLibraryAsync); + }} > @@ -216,62 +189,76 @@ export default function HomeScreen() { - - {/* Highlights Section */} - - - Highlights - - - - {foodHighlights.map((food) => ( - navigateToFoodDetail(String(food.id))} - > - - - - {food.name} - - - {food.description} - - - - - - {food.time} - - - - - - {food.calories} - - - - - - ))} - - + + + Highlights + + + {isLoading ? ( + + Loading highlights... + + ) : error ? ( + + Failed to load highlights + + ) : filteredFoods.length === 0 ? ( + + No highlights available + + ) : ( + + {filteredFoods.map((food, idx) => ( + navigateToFoodDetail(food.id)} + > + {food.image_url ? ( + + ) : ( + + No Image + + )} + + + {food.name} + + + {food.description || "No description"} + + + + + + {food.time_to_cook_minutes + ? `${food.time_to_cook_minutes} min` + : "-"} + + + + + + ))} + + )} + + {/* Extra space at bottom */} diff --git a/package-lock.json b/package-lock.json index 8a6bc5c..00557b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,17 +11,23 @@ "@dev-plugins/react-query": "^0.3.1", "@expo/ngrok": "^4.1.3", "@expo/vector-icons": "^14.1.0", + "@google/genai": "^0.13.0", "@react-native-async-storage/async-storage": "2.1.2", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", "@supabase/supabase-js": "2.49.5-next.1", + "base64-arraybuffer": "^1.0.2", + "cors": "^2.8.5", + "dotenv": "^16.5.0", "expo": "^53.0.9", "expo-blur": "~14.1.4", "expo-constants": "~17.1.6", + "expo-file-system": "~18.1.9", "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", "expo-image": "~2.1.7", + "expo-image-manipulator": "~13.1.6", "expo-image-picker": "~16.1.4", "expo-linking": "~7.1.4", "expo-router": "~5.0.6", @@ -31,6 +37,8 @@ "expo-symbols": "~0.4.4", "expo-system-ui": "~5.0.7", "expo-web-browser": "~14.1.6", + "express": "^5.1.0", + "mime": "^4.0.7", "nativewind": "^4.1.23", "react": "19.0.0", "react-dom": "19.0.0", @@ -1863,6 +1871,18 @@ "getenv": "^1.0.0" } }, + "node_modules/@expo/env/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@expo/fingerprint": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.12.4.tgz", @@ -1996,6 +2016,18 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@expo/metro-config/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@expo/metro-config/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2334,6 +2366,21 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@google/genai": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz", + "integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4538,6 +4585,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4594,6 +4650,15 @@ "node": ">=0.6" } }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4610,7 +4675,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -4728,6 +4792,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4788,7 +4858,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -4801,7 +4870,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -5234,7 +5302,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -5246,7 +5313,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -5260,7 +5326,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -5269,7 +5334,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, "engines": { "node": ">=6.6.0" } @@ -5296,7 +5360,7 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -5653,9 +5717,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -5681,7 +5746,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -5696,6 +5760,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5821,7 +5894,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5830,7 +5902,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5866,7 +5937,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -6517,6 +6587,18 @@ "expo": "*" } }, + "node_modules/expo-image-manipulator": { + "version": "13.1.7", + "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-13.1.7.tgz", + "integrity": "sha512-DBy/Xdd0E/yFind14x36XmwfWuUxOHI/oH97/giKjjPaRc2dlyjQ3tuW3x699hX6gAs9Sixj5WEJ1qNf3c8sag==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~5.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-image-picker": { "version": "16.1.4", "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.1.4.tgz", @@ -6710,7 +6792,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, + "license": "MIT", "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6767,7 +6849,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -6780,7 +6861,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -6797,7 +6877,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -6809,7 +6888,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -6818,7 +6896,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -6840,7 +6917,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -6851,6 +6927,12 @@ "node": ">= 18" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7130,7 +7212,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -7148,7 +7229,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -7208,6 +7288,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7228,7 +7351,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -7260,7 +7382,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -7396,11 +7517,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7443,6 +7589,19 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -7503,7 +7662,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7634,7 +7792,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -7772,7 +7929,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "engines": { "node": ">= 0.10" } @@ -8071,8 +8227,7 @@ "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, "node_modules/is-regex": { "version": "1.2.1", @@ -8119,6 +8274,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -8502,6 +8669,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -8550,6 +8726,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9010,7 +9207,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -9019,7 +9215,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, "engines": { "node": ">= 0.8" } @@ -9033,7 +9228,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, "engines": { "node": ">=18" }, @@ -9396,14 +9590,18 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", + "integrity": "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { @@ -9705,7 +9903,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -10150,7 +10347,6 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, "engines": { "node": ">=16" } @@ -10490,7 +10686,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -10528,7 +10723,6 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -10606,7 +10800,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -11316,7 +11509,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -11424,8 +11616,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { "version": "1.4.1", @@ -11542,6 +11733,18 @@ "node": ">= 0.6" } }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/serialize-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", @@ -11585,6 +11788,18 @@ "node": ">= 0.6" } }, + "node_modules/serve-static/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/serve-static/node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -11724,7 +11939,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -11743,7 +11957,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -11759,7 +11972,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -11777,7 +11989,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12560,7 +12771,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -12574,7 +12784,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, "dependencies": { "mime-db": "^1.54.0" }, @@ -13350,7 +13559,6 @@ "version": "3.24.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -13359,7 +13567,6 @@ "version": "3.24.5", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index dee44a8..78a81c2 100644 --- a/package.json +++ b/package.json @@ -14,17 +14,23 @@ "@dev-plugins/react-query": "^0.3.1", "@expo/ngrok": "^4.1.3", "@expo/vector-icons": "^14.1.0", + "@google/genai": "^0.13.0", "@react-native-async-storage/async-storage": "2.1.2", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", "@supabase/supabase-js": "2.49.5-next.1", + "base64-arraybuffer": "^1.0.2", + "cors": "^2.8.5", + "dotenv": "^16.5.0", "expo": "^53.0.9", "expo-blur": "~14.1.4", "expo-constants": "~17.1.6", + "expo-file-system": "~18.1.9", "expo-font": "~13.3.1", "expo-haptics": "~14.1.4", "expo-image": "~2.1.7", + "expo-image-manipulator": "~13.1.6", "expo-image-picker": "~16.1.4", "expo-linking": "~7.1.4", "expo-router": "~5.0.6", @@ -34,6 +40,8 @@ "expo-symbols": "~0.4.4", "expo-system-ui": "~5.0.7", "expo-web-browser": "~14.1.6", + "express": "^5.1.0", + "mime": "^4.0.7", "nativewind": "^4.1.23", "react": "19.0.0", "react-dom": "19.0.0", diff --git a/services/data/foods.ts b/services/data/foods.ts index 2237314..0f68eb5 100644 --- a/services/data/foods.ts +++ b/services/data/foods.ts @@ -1,5 +1,5 @@ import { supabase } from "@/services/supabase"; -import { Foods, LikedFood, SavedFood } from "@/types"; +import { Foods, GenAIResult, LikedFood, SavedFood } from "@/types"; import { PostgrestError } from "@supabase/supabase-js"; /** @@ -125,4 +125,91 @@ export const getIngredients = async (foodId: string): Promise<{ data: Ingredient `) .eq("food_id", foodId) return { data, error }; -}; \ No newline at end of file +}; + + +/** + * Inserts a new food into the database. + * + * @param genAIResult - The result from the GenAI API. + * @param userId - The ID of the user who created the food. + * @param imageUrl - The URL of the image of the food. + * @returns A promise that resolves to an object containing the ID of the inserted food and any error that occurred. + */ +export const insertGenAIResult = async ( + genAIResult: GenAIResult, + userId: string, + imageUrl: string +): Promise<{ data: string | null; error: PostgrestError | null }> => { + const client = supabase; + + const now = new Date().toISOString(); + + const { foods, ingredients, nutrients, cooking_steps } = genAIResult; + + const { data: foodInsert, error: foodError } = await client + .from("foods") + .insert({ + name: foods.name, + description: foods.description, + time_to_cook_minutes: foods.time_to_cook_minutes, + skill_level: foods.skill_level, + ingredient_count: foods.ingredient_count, + calories: foods.calories, + image_url: imageUrl, + is_shared: false, + created_by: userId, + created_at: now, + }) + .select("id") + .single(); + + if (foodError || !foodInsert) { + return { data: null, error: foodError }; + } + + const foodId = foodInsert.id; + + const { error: nutrientError } = await client.from("nutrients").insert({ + food_id: foodId, + ...nutrients, + created_at: now, + }); + + if (nutrientError) { + return { data: null, error: nutrientError }; + } + + const ingredientInsert = ingredients.map((i) => ({ + food_id: foodId, + name: i.name, + emoji: i.emoji, + created_at: now, + })); + + const { error: ingredientError } = await client + .from("ingredients") + .insert(ingredientInsert); + + if (ingredientError) { + return { data: null, error: ingredientError }; + } + + const stepInsert = cooking_steps.map((step) => ({ + food_id: foodId, + step_order: step.step_order, + title: step.title, + description: step.description, + created_at: now, + })); + + const { error: stepError } = await client + .from("cooking_steps") + .insert(stepInsert); + + if (stepError) { + return { data: null, error: stepError }; + } + + return { data: foodId, error: null }; +}; \ No newline at end of file diff --git a/services/data/imageUpload.ts b/services/data/imageUpload.ts new file mode 100644 index 0000000..4bef0b1 --- /dev/null +++ b/services/data/imageUpload.ts @@ -0,0 +1,36 @@ +import { decode } from "base64-arraybuffer"; +import { supabase } from "../supabase"; +export async function uploadImageToSupabase(imageBase64: string, imageType: string, userId: string): Promise { + if (!userId) { + throw new Error("User ID is required."); + } + + const filePath = `${userId}/${new Date().getTime()}.${imageType === "image" ? "png" : "jpg"}`; + const contentType = imageType === "image" ? "image/png" : "image/jpeg"; + + const { error: uploadError } = await supabase + .storage + .from("food") + .upload(filePath, decode(imageBase64), { + contentType: contentType, + cacheControl: "3600", + upsert: false, + }); + + if (uploadError) { + console.error("[UPLOAD ERROR]", uploadError); + throw uploadError; + } + + const { data, error } = await supabase + .storage + .from("food") + .createSignedUrl(filePath, 31536000); + + if (error) { + console.error("[GET PUBLIC URL ERROR]", error); + throw error; + } + + return data.signedUrl; +} diff --git a/services/gemini.ts b/services/gemini.ts new file mode 100644 index 0000000..def8847 --- /dev/null +++ b/services/gemini.ts @@ -0,0 +1,10 @@ +import { GenAIResult } from '../types'; +import { supabase } from './supabase'; + +export async function callGenAIonImage(imageUrl: string): Promise<{ data: GenAIResult | null; error: Error | null }> { + const { data, error } = await supabase.functions.invoke('gemini-food-analyze', { + body: { imageUrl: imageUrl }, + }) + + return { data, error } +} \ No newline at end of file diff --git a/services/supabase.ts b/services/supabase.ts index 1fff9ee..4c64a52 100644 --- a/services/supabase.ts +++ b/services/supabase.ts @@ -1,12 +1,25 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as SecureStore from 'expo-secure-store'; +// import AsyncStorage from '@react-native-async-storage/async-storage'; import { createClient } from '@supabase/supabase-js'; const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_PROJECT_URL as string; const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY as string; +const ExpoSecureStoreAdapter = { + getItem: (key: string) => { + return SecureStore.getItemAsync(key); + }, + setItem: (key: string, value: string) => { + SecureStore.setItemAsync(key, value); + }, + removeItem: (key: string) => { + SecureStore.deleteItemAsync(key); + }, +}; + export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { - storage: AsyncStorage, + storage: ExpoSecureStoreAdapter as any, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, diff --git a/types.ts b/types.ts index 3d03231..baeb4a5 100644 --- a/types.ts +++ b/types.ts @@ -33,4 +33,31 @@ interface CookingStep { description: string } -export { CookingStep, Foods, LikedFood, SavedFood }; +interface GenAIResult { + foods: { + name: string; + description: string; + time_to_cook_minutes: number; + skill_level: "Easy" | "Medium" | "Hard"; + ingredient_count: number; + calories: number; + }; + cooking_steps: { + title: string; + description: string; + step_order: number; + }[]; + ingredients: { + name: string; + emoji: string; + }[]; + nutrients: { + fat_g: number; + fiber_g: number; + protein_g: number; + carbs_g: number; + }; +} + +export { CookingStep, Foods, GenAIResult, LikedFood, SavedFood }; +