fix groceries, todo, calendar

This commit is contained in:
ivic00
2024-09-25 20:45:14 +02:00
parent 1533ec525b
commit 857e60a3d3
15 changed files with 1923 additions and 857 deletions

View File

@ -1,6 +1,3 @@
import { Text, View } from "react-native-ui-lib";
import GroceryList from "@/components/pages/grocery/GroceryList";
import AddGroceryItem from "@/components/pages/grocery/AddGroceryItem";
import { GroceryProvider } from "@/contexts/GroceryContext"; import { GroceryProvider } from "@/contexts/GroceryContext";
import React from "react"; import React from "react";
import GroceryWrapper from "@/components/pages/grocery/GroceryWrapper"; import GroceryWrapper from "@/components/pages/grocery/GroceryWrapper";

View File

@ -51,7 +51,7 @@ export const AddEventDialog = () => {
}} }}
onPress={handleOpenManualInputModal} onPress={handleOpenManualInputModal}
> >
<Text style={{color: "white"}}>Manually Input</Text> <Text style={{color: "white"}}>Create New</Text>
</Button> </Button>
<Button <Button
@ -62,7 +62,7 @@ export const AddEventDialog = () => {
}} }}
disabled disabled
> >
<Text style={{color: "white"}}>Scan an Image</Text> <Text style={{color: "white"}}>Event</Text>
</Button> </Button>
<Button <Button
@ -73,7 +73,18 @@ export const AddEventDialog = () => {
}} }}
disabled disabled
> >
<Text style={{color: "white"}}>Paste in text</Text> <Text style={{color: "white"}}>To Do</Text>
</Button>
<Button
style={{
marginBottom: 10,
backgroundColor: "#007bff",
opacity: 0.5
}}
disabled
>
<Text style={{color: "white"}}>Upload Image</Text>
</Button> </Button>
</View> </View>

View File

@ -1,260 +1,320 @@
import { import {
Avatar, Avatar,
Colors, Colors,
DateTimePicker, DateTimePicker,
LoaderScreen, LoaderScreen,
Modal, Modal,
Picker, Picker,
Switch, Switch,
Text, Text,
TextField, TextField,
View View,
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import {ScrollView, TouchableOpacity} from "react-native-gesture-handler"; import { ScrollView, TouchableOpacity } from "react-native-gesture-handler";
import {useSafeAreaInsets} from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import {useState} from "react"; import { useState } from "react";
import {MaterialIcons} from "@expo/vector-icons"; import { MaterialIcons } from "@expo/vector-icons";
import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types"; import { PickerMultiValue } from "react-native-ui-lib/src/components/picker/types";
import {useAuthContext} from "@/contexts/AuthContext"; import { useAuthContext } from "@/contexts/AuthContext";
import {useCreateEvent} from "@/hooks/firebase/useCreateEvent"; import { useCreateEvent } from "@/hooks/firebase/useCreateEvent";
import {EventData} from "@/hooks/firebase/types/eventData"; import { EventData } from "@/hooks/firebase/types/eventData";
const daysOfWeek = [ const daysOfWeek = [
{label: "Monday", value: "monday"}, { label: "Monday", value: "monday" },
{label: "Tuesday", value: "tuesday"}, { label: "Tuesday", value: "tuesday" },
{label: "Wednesday", value: "wednesday"}, { label: "Wednesday", value: "wednesday" },
{label: "Thursday", value: "thursday"}, { label: "Thursday", value: "thursday" },
{label: "Friday", value: "friday"}, { label: "Friday", value: "friday" },
{label: "Saturday", value: "saturday"}, { label: "Saturday", value: "saturday" },
{label: "Sunday", value: "sunday"}, { label: "Sunday", value: "sunday" },
]; ];
export const ManuallyAddEventModal = ({show, close}: { show: boolean, close: () => void }) => { export const ManuallyAddEventModal = ({
const {user} = useAuthContext() show,
const insets = useSafeAreaInsets(); close,
}: {
show: boolean;
close: () => void;
}) => {
const { user } = useAuthContext();
const insets = useSafeAreaInsets();
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [isAllDay, setIsAllDay] = useState(false); const [isAllDay, setIsAllDay] = useState(false);
const [startTime, setStartTime] = useState(new Date()); const [startTime, setStartTime] = useState(new Date());
const [endTime, setEndTime] = useState(new Date()) const [endTime, setEndTime] = useState(new Date());
const [startDate, setStartDate] = useState(new Date()); const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date()) const [endDate, setEndDate] = useState(new Date());
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]); const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
const {mutateAsync: createEvent, isLoading, isError} = useCreateEvent() const { mutateAsync: createEvent, isLoading, isError } = useCreateEvent();
const formatDateTime = (date: Date) => { const formatDateTime = (date: Date) => {
return date.toLocaleDateString('en-US', { return date.toLocaleDateString("en-US", {
weekday: 'long', weekday: "long",
month: 'short', month: "short",
day: 'numeric' day: "numeric",
}); });
}; };
const combineDateAndTime = (date: Date, time: Date): Date => { const combineDateAndTime = (date: Date, time: Date): Date => {
const combined = new Date(date); const combined = new Date(date);
combined.setHours(time.getHours()); combined.setHours(time.getHours());
combined.setMinutes(time.getMinutes()); combined.setMinutes(time.getMinutes());
combined.setSeconds(0); combined.setSeconds(0);
combined.setMilliseconds(0); combined.setMilliseconds(0);
return combined; return combined;
}; };
const handleSave = async () => { const handleSave = async () => {
let finalStartDate: Date; let finalStartDate: Date;
let finalEndDate: Date; let finalEndDate: Date;
if (isAllDay) { if (isAllDay) {
finalStartDate = new Date(startDate); finalStartDate = new Date(startDate);
finalStartDate.setHours(0, 0, 0, 0); finalStartDate.setHours(0, 0, 0, 0);
finalEndDate = new Date(startDate); finalEndDate = new Date(startDate);
finalEndDate.setHours(23, 59, 59, 999); finalEndDate.setHours(23, 59, 59, 999);
} else { } else {
finalStartDate = combineDateAndTime(startDate, startTime); finalStartDate = combineDateAndTime(startDate, startTime);
finalEndDate = combineDateAndTime(endDate, endTime); finalEndDate = combineDateAndTime(endDate, endTime);
}
const eventData: Partial<EventData> = {
title,
startDate: finalStartDate,
endDate: finalEndDate,
repeatDays: repeatInterval.map(x => x.toString()),
allDay: isAllDay
};
await createEvent(eventData)
close();
};
const getRepeatLabel = () => {
const selectedDays = repeatInterval
const allDays = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
const isEveryWorkDay = workDays.every(day => selectedDays.includes(day));
const isEveryDay = allDays.every(day => selectedDays.includes(day));
if (isEveryDay) {
return "Every day";
} else if (isEveryWorkDay && !selectedDays.includes("saturday") && !selectedDays.includes("sunday")) {
return "Every work day";
} else {
return selectedDays
.map(item => daysOfWeek.find(day => day.value === item)?.label)
.join(", ");
}
};
if (isLoading && !isError) {
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<LoaderScreen message={'Saving event...'} color={Colors.grey40}/>
</Modal>
)
} }
const eventData: Partial<EventData> = {
title,
startDate: finalStartDate,
endDate: finalEndDate,
repeatDays: repeatInterval.map((x) => x.toString()),
allDay: isAllDay,
};
await createEvent(eventData);
close();
};
const getRepeatLabel = () => {
const selectedDays = repeatInterval;
const allDays = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
];
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
const isEveryWorkDay = workDays.every((day) => selectedDays.includes(day));
const isEveryDay = allDays.every((day) => selectedDays.includes(day));
if (isEveryDay) {
return "Every day";
} else if (
isEveryWorkDay &&
!selectedDays.includes("saturday") &&
!selectedDays.includes("sunday")
) {
return "Every work day";
} else {
return selectedDays
.map((item) => daysOfWeek.find((day) => day.value === item)?.label)
.join(", ");
}
};
if (isLoading && !isError) {
return ( return (
<Modal <Modal
visible={show} visible={show}
animationType="slide" animationType="slide"
onRequestClose={close} onRequestClose={close}
transparent={false} transparent={false}
> >
<View style={{ <LoaderScreen message={"Saving event..."} color={Colors.grey40} />
flex: 1, </Modal>
backgroundColor: "#fff",
paddingTop: insets.top, // Safe area inset for top
paddingBottom: insets.bottom, // Safe area inset for bottom
paddingLeft: insets.left, // Safe area inset for left
paddingRight: insets.right, // Safe area inset for right
}}>
<View style={{flexDirection: "row", justifyContent: "space-between", padding: 16}}>
<TouchableOpacity onPress={close}>
<Text style={{color: "#007bff"}}>Cancel</Text>
</TouchableOpacity>
<Text style={{fontWeight: "bold", fontSize: 16}}>Add event</Text>
<TouchableOpacity onPress={handleSave}>
<Text style={{color: "#007bff"}}>Save</Text>
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={{paddingHorizontal: 16, paddingTop: 10}}>
<View style={{marginVertical: 10}}>
<TextField
placeholder={'Title'}
floatingPlaceholder
value={title}
onChangeText={setTitle}
showCharCounter
maxLength={200}
fieldStyle={{
borderBottomWidth: 1,
borderBottomColor: 'black',
borderStyle: 'solid',
}}
/>
</View>
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20
}}>
<View style={{flexDirection: "row", alignItems: "center"}}>
<MaterialIcons name="schedule" size={24} color="gray"/>
<Text style={{marginLeft: 10}}>All-day</Text>
</View>
<Switch
value={isAllDay}
onValueChange={(value) => setIsAllDay(value)}
/>
</View>
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20
}}>
<DateTimePicker
mode="date"
dateFormatter={formatDateTime}
value={startDate}
onChange={setStartDate}
/>
{!isAllDay && (
<DateTimePicker
mode="time"
value={startTime}
onChange={setStartTime}
/>
)}
</View>
{!isAllDay && (
<View style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20
}}>
<DateTimePicker
mode="date"
dateFormatter={formatDateTime}
value={endDate}
onChange={setEndDate}
/>
<DateTimePicker
mode="time"
value={endTime}
onChange={setEndTime}
/>
</View>
)}
<View style={{flexDirection: "row", alignItems: "center", marginBottom: 20}}>
<MaterialIcons name="repeat" size={24} color="gray"/>
<Picker
value={repeatInterval}
//@ts-ignore
onChange={(items: PickerMultiValue) => setRepeatInterval(items)}
placeholder="Doest not repeat"
style={{marginLeft: 10, flex: 1}}
mode={Picker.modes.MULTI}
getLabel={getRepeatLabel}
>
{daysOfWeek.map((option) => (
<Picker.Item key={option.value} label={option.label} value={option.value}/>
))}
</Picker>
</View>
<View style={{flexDirection: "row", alignItems: "center", marginBottom: 20}}>
<MaterialIcons name="person-add" size={24} color="gray"/>
<TouchableOpacity style={{marginLeft: 10, flexDirection: "row", alignItems: "center", flex: 1}}>
<Avatar size={40} backgroundColor={Colors.yellow10}/>
<View style={{marginLeft: 10}}>
<Text>Other</Text>
<Text style={{color: "gray"}}>{user?.email}</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView>
</View>
</Modal>
); );
}
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<View
style={{
flex: 1,
backgroundColor: "#fff",
paddingTop: insets.top, // Safe area inset for top
paddingBottom: insets.bottom, // Safe area inset for bottom
paddingLeft: insets.left, // Safe area inset for left
paddingRight: insets.right, // Safe area inset for right
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
padding: 16,
}}
>
<TouchableOpacity onPress={close}>
<Text style={{ color: "#007bff" }}>Cancel</Text>
</TouchableOpacity>
<Text style={{ fontWeight: "bold", fontSize: 16 }}>Add event</Text>
<TouchableOpacity onPress={handleSave}>
<Text style={{ color: "#007bff" }}>Save</Text>
</TouchableOpacity>
</View>
<ScrollView
contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 10 }}
>
<View style={{ marginVertical: 10 }}>
<TextField
placeholder={"Title"}
floatingPlaceholder
value={title}
onChangeText={setTitle}
showCharCounter
maxLength={200}
fieldStyle={{
borderBottomWidth: 1,
borderBottomColor: "black",
borderStyle: "solid",
}}
/>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
}}
>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<MaterialIcons name="schedule" size={24} color="gray" />
<Text style={{ marginLeft: 10 }}>All-day</Text>
</View>
<Switch
value={isAllDay}
onValueChange={(value) => setIsAllDay(value)}
/>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
}}
>
<DateTimePicker
mode="date"
dateFormatter={formatDateTime}
value={startDate}
onChange={setStartDate}
display="default"
/>
{!isAllDay && (
<DateTimePicker
mode="time"
value={startTime}
onChange={setStartTime}
display="spinner"
/>
)}
</View>
{!isAllDay && (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
}}
>
<DateTimePicker
mode="date"
dateFormatter={formatDateTime}
value={endDate}
onChange={setEndDate}
display="default"
/>
<DateTimePicker
mode="time"
value={endTime}
onChange={setEndTime}
minuteInterval={1}
display="spinner"
/>
</View>
)}
<View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 20,
}}
>
<MaterialIcons name="repeat" size={24} color="gray" />
<Picker
value={repeatInterval}
//@ts-ignore
onChange={(items: PickerMultiValue) => setRepeatInterval(items)}
placeholder="Doest not repeat"
style={{ marginLeft: 10, flex: 1 }}
mode={Picker.modes.MULTI}
getLabel={getRepeatLabel}
>
{daysOfWeek.map((option) => (
<Picker.Item
key={option.value}
label={option.label}
value={option.value}
/>
))}
</Picker>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 20,
}}
>
<MaterialIcons name="person-add" size={24} color="gray" />
<TouchableOpacity
style={{
marginLeft: 10,
flexDirection: "row",
alignItems: "center",
flex: 1,
}}
>
<Avatar size={40} backgroundColor={Colors.yellow10} />
<View style={{ marginLeft: 10 }}>
<Text>Other</Text>
<Text style={{ color: "gray" }}>{user?.email}</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView>
</View>
</Modal>
);
}; };

View File

@ -1,28 +1,60 @@
import { View, Text } from 'react-native' import { View, Text } from "react-native";
import React from 'react' import React, { useEffect, useState } from "react";
import { TextField } from 'react-native-ui-lib' import { TextField } from "react-native-ui-lib";
import CategoryDropdown from './CategoryDropdown' import {
import { GroceryCategory } from '@/contexts/GroceryContext' GroceryCategory,
IGrocery,
useGroceryContext,
} from "@/contexts/GroceryContext";
import { TouchableWithoutFeedback } from "react-native-gesture-handler";
const EditGroceryItem = (props: {title: string, setTitle: (value: string) => void, setCategory: (category: GroceryCategory) => void}) => { interface IEditGrocery {
return ( id?: number;
<View title: string;
style={{ setTitle: (value: string) => void;
backgroundColor: "white", setCategory?: (category: GroceryCategory) => void;
width: "100%", category: GroceryCategory;
borderRadius: 25, setSubmit?: (value: boolean) => void;
padding: 15, updateCategory?: (id: number, changes: Partial<IGrocery>) => void;
}} closeEdit?: (value: boolean) => void;
>
<TextField
placeholder="Grocery"
value={props.title}
onChangeText={(value) => props.setTitle(value)}
maxLength={25}
/>
<CategoryDropdown setSelectedCategory={props.setCategory} />
</View>
)
} }
export default EditGroceryItem const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
const { fuzzyMatchGroceryCategory } = useGroceryContext();
useEffect(() => {
if (editGrocery.setCategory)
editGrocery.setCategory(fuzzyMatchGroceryCategory(editGrocery.title));
}, [editGrocery.title]);
return (
<View
style={{
backgroundColor: "white",
width: "100%",
borderRadius: 25,
padding: 15,
}}
>
<TextField
placeholder="Grocery"
value={editGrocery.title}
onChangeText={(value) => {
editGrocery.setTitle(value);
}}
onSubmitEditing={() => {
if (editGrocery.setSubmit) editGrocery.setSubmit(true);
if (editGrocery.closeEdit) editGrocery.closeEdit(false);
if (editGrocery.updateCategory && editGrocery.id)
editGrocery.updateCategory(editGrocery.id, {
category: fuzzyMatchGroceryCategory(editGrocery.title), title: editGrocery.title
});
}}
maxLength={25}
/>
<Text>{editGrocery.category}</Text>
</View>
);
};
export default EditGroceryItem;

View File

@ -17,6 +17,7 @@ import {
import EditGroceryFrequency from "./EditGroceryFrequency"; import EditGroceryFrequency from "./EditGroceryFrequency";
import { TextInput } from "react-native"; import { TextInput } from "react-native";
import EditGroceryItem from "./EditGroceryItem"; import EditGroceryItem from "./EditGroceryItem";
import { TouchableWithoutFeedback } from "react-native-gesture-handler";
const GroceryItem = ({ const GroceryItem = ({
item, item,
@ -31,6 +32,7 @@ const GroceryItem = ({
const [openFreqEdit, setOpenFreqEdit] = useState<boolean>(false); const [openFreqEdit, setOpenFreqEdit] = useState<boolean>(false);
const [isEditingTitle, setIsEditingTitle] = useState<boolean>(false); const [isEditingTitle, setIsEditingTitle] = useState<boolean>(false);
const [newTitle, setNewTitle] = useState<string>(""); const [newTitle, setNewTitle] = useState<string>("");
const [category, setCategory] = useState<GroceryCategory>(GroceryCategory.None);
const handleTitleChange = (newTitle: string) => { const handleTitleChange = (newTitle: string) => {
updateGroceryItem(item.id, { title: newTitle }); updateGroceryItem(item.id, { title: newTitle });
@ -46,6 +48,7 @@ const GroceryItem = ({
return ( return (
<ListItem <ListItem
key={item.id}
style={{ borderRadius: 50, marginVertical: 5, height: 55 }} style={{ borderRadius: 50, marginVertical: 5, height: 55 }}
backgroundColor="white" backgroundColor="white"
centerV centerV
@ -63,49 +66,24 @@ const GroceryItem = ({
}} }}
/> />
<ListItem.Part left containerStyle={{ flex: 1, paddingStart: 20 }}> <ListItem.Part left containerStyle={{ flex: 1, paddingStart: 20 }}>
{/* <View
height={50}
width={50}
style={{ borderRadius: 15 }}
backgroundColor="#e6f1ed"
marginR-10
children={
<MaterialCommunityIcons
name={iconMapping[item.category]}
size={50}
color="orange"
/>
}
/>*/}
{!isEditingTitle ? ( {!isEditingTitle ? (
<View> <View>
<TouchableOpacity onPress={() => setIsEditingTitle(true)}> <TouchableOpacity onPress={() => setIsEditingTitle(true)}>
<Text text70BL>{item.title}</Text> <Text text70BL>{item.title}</Text>
</TouchableOpacity> </TouchableOpacity>
{ /*
<TextInput
value={item.title}
onChangeText={handleTitleChange}
onBlur={() => {
setIsEditingTitle(false);
console.log(groceries);
}}
autoFocus
style={{
fontSize: 16,
padding: 0,
margin: 0,
fontWeight: "bold",
}}
/>
*/}
</View> </View>
) : ( ) : (
<EditGroceryItem <EditGroceryItem
title={item.title} editGrocery={{
setTitle={handleTitleChange} id: item.id,
setCategory={handleCategoryChange} title: newTitle,
/> setTitle: setNewTitle,
category:category,
updateCategory: updateGroceryItem,
closeEdit: setIsEditingTitle,
setCategory: setCategory,
}}
/>
)} )}
</ListItem.Part> </ListItem.Part>
<ListItem.Part right containerStyle={{ paddingEnd: 20 }}> <ListItem.Part right containerStyle={{ paddingEnd: 20 }}>

View File

@ -31,6 +31,7 @@ const GroceryList = () => {
GroceryCategory.Bakery GroceryCategory.Bakery
); );
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [submit, setSubmitted] = useState<boolean>(false);
// Group approved groceries by category // Group approved groceries by category
const approvedGroceriesByCategory = approvedGroceries.reduce( const approvedGroceriesByCategory = approvedGroceries.reduce(
@ -46,19 +47,27 @@ const GroceryList = () => {
); );
useEffect(() => { useEffect(() => {
if (title?.length > 2 && title?.length <= 25) { if(submit){
addGrocery({ if (title?.length > 2 && title?.length <= 25) {
id: 0, addGrocery({
title: title, id: 0,
category: category, title: title,
approved: false, category: category,
recurring: false, approved: false,
frequency: GroceryFrequency.Never, recurring: false,
bought: false, frequency: GroceryFrequency.Never,
}); bought: false,
});
setIsAddingGrocery(false);
setIsAddingGrocery(false);
setSubmitted(false);
setTitle("");
}
} }
}, [submit]);
useEffect(() => {
/**/
}, [category]); }, [category]);
useEffect(() => { useEffect(() => {
@ -67,7 +76,7 @@ const GroceryList = () => {
}, [groceries]); }, [groceries]);
return ( return (
<View marginH-20> <View marginH-20 marginB-20>
<HeaderTemplate <HeaderTemplate
message={"Welcome to your grocery list"} message={"Welcome to your grocery list"}
isWelcome={false} isWelcome={false}
@ -101,8 +110,8 @@ const GroceryList = () => {
</Text> </Text>
</View> </View>
<Button <Button
backgroundColor='transparent' backgroundColor="transparent"
paddingH-10 paddingH-10
iconSource={() => ( iconSource={() => (
<MaterialIcons name="person-add-alt" size={24} color="gray" /> <MaterialIcons name="person-add-alt" size={24} color="gray" />
)} )}
@ -157,7 +166,15 @@ const GroceryList = () => {
</View> </View>
</View> </View>
{isAddingGrocery && ( {isAddingGrocery && (
<EditGroceryItem title={title} setTitle={setTitle} setCategory={setCategory} /> <EditGroceryItem
editGrocery={{
title: title,
setCategory: setCategory,
category: category,
setTitle: setTitle,
setSubmit: setSubmitted
}}
/>
)} )}
{/* Render Approved Groceries Grouped by Category */} {/* Render Approved Groceries Grouped by Category */}

View File

@ -1,7 +1,12 @@
import { View, Text, TouchableOpacity } from "react-native-ui-lib"; import { View, Text, TouchableOpacity } from "react-native-ui-lib";
import React, { useState } from "react"; import React, { useState } from "react";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { ScrollView, StyleSheet } from "react-native"; import {
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
} from "react-native";
import MyProfile from "./user_settings_views/MyProfile"; import MyProfile from "./user_settings_views/MyProfile";
import MyGroup from "./user_settings_views/MyGroup"; import MyGroup from "./user_settings_views/MyGroup";
import { useAuthContext } from "@/contexts/AuthContext"; import { useAuthContext } from "@/contexts/AuthContext";
@ -9,51 +14,57 @@ import { useAuthContext } from "@/contexts/AuthContext";
const UserSettings = (props: { setSelectedPage: (page: number) => void }) => { const UserSettings = (props: { setSelectedPage: (page: number) => void }) => {
const [selectedView, setSelectedView] = useState<boolean>(true); const [selectedView, setSelectedView] = useState<boolean>(true);
return ( return (
<View> <ScrollView
<ScrollView> contentContainerStyle={{ flexGrow: 1 }}
<TouchableOpacity onPress={() => props.setSelectedPage(0)}> >
<View row marginT-20 marginL-20 marginB-35 centerV> <View style={{ flex: 1 }}>
<Ionicons name="chevron-back" size={22} color="#979797" /> <TouchableOpacity onPress={() => props.setSelectedPage(0)}>
<Text text70 color="#979797"> <View row marginT-20 marginL-20 marginB-35 centerV>
Return to main settings <Ionicons name="chevron-back" size={22} color="#979797" />
<Text text70 color="#979797">
Return to main settings
</Text>
</View>
</TouchableOpacity>
<View marginH-20>
<Text text60R marginB-25>
User Management
</Text> </Text>
<View style={styles.buttonSwitch} spread row>
<TouchableOpacity
onPress={() => setSelectedView(true)}
centerV
centerH
style={
selectedView == true ? styles.btnSelected : styles.btnNot
}
>
<View>
<Text text70 color={selectedView ? "white" : "black"}>
My Profile
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setSelectedView(false)}
centerV
centerH
style={
selectedView == false ? styles.btnSelected : styles.btnNot
}
>
<View>
<Text text70 color={!selectedView ? "white" : "black"}>
My Group
</Text>
</View>
</TouchableOpacity>
</View>
{selectedView && <MyProfile />}
{!selectedView && <MyGroup />}
</View> </View>
</TouchableOpacity>
<View marginH-20>
<Text text60R marginB-25>
User Management
</Text>
<View style={styles.buttonSwitch} spread row>
<TouchableOpacity
onPress={() => setSelectedView(true)}
centerV
centerH
style={selectedView == true ? styles.btnSelected : styles.btnNot}
>
<View>
<Text text70 color={selectedView ? "white" : "black"}>
My Profile
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setSelectedView(false)}
centerV
centerH
style={selectedView == false ? styles.btnSelected : styles.btnNot}
>
<View>
<Text text70 color={!selectedView ? "white" : "black"}>
My Group
</Text>
</View>
</TouchableOpacity>
</View>
{selectedView && <MyProfile />}
{!selectedView && <MyGroup />}
</View> </View>
</ScrollView> </ScrollView>
</View>
); );
}; };

View File

@ -2,8 +2,10 @@ import { View, Text, Button } from "react-native-ui-lib";
import React from "react"; import React from "react";
import { Fontisto } from "@expo/vector-icons"; import { Fontisto } from "@expo/vector-icons";
import { ProgressBar } from "react-native-ui-lib/src/components/progressBar"; import { ProgressBar } from "react-native-ui-lib/src/components/progressBar";
import { useToDosContext } from "@/contexts/ToDosContext";
const ProgressCard = () => { const ProgressCard = () => {
const { maxPoints } = useToDosContext();
return ( return (
<View <View
backgroundColor="white" backgroundColor="white"
@ -30,12 +32,10 @@ const ProgressCard = () => {
/> />
<View row spread> <View row spread>
<Text color={"#868686"}>0</Text> <Text color={"#868686"}>0</Text>
<Text color={"#868686"}>1000</Text> <Text color={"#868686"}>{maxPoints}</Text>
</View> </View>
<View centerV centerH> <View centerV centerH>
<Button <Button backgroundColor="transparent">
backgroundColor="transparent"
>
<Text style={{ textDecorationLine: "underline", color: "#05a8b6" }}> <Text style={{ textDecorationLine: "underline", color: "#05a8b6" }}>
View your full progress report here View your full progress report here
</Text> </Text>

View File

@ -1,5 +1,6 @@
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import { createContext, useContext, useState } from "react"; import { createContext, useContext, useState } from "react";
import fuzzysort from "fuzzysort";
export enum GroceryFrequency { export enum GroceryFrequency {
Never = "Never", Never = "Never",
@ -32,9 +33,10 @@ export enum GroceryCategory {
Household = "Household", Household = "Household",
PersonalCare = "Personal Care", PersonalCare = "Personal Care",
Frozen = "Frozen", Frozen = "Frozen",
None = "No Category",
} }
type MaterialIconNames = keyof typeof MaterialCommunityIcons.glyphMap; /*type MaterialIconNames = keyof typeof MaterialCommunityIcons.glyphMap;
const iconMapping: { [key in GroceryCategory]: MaterialIconNames } = { const iconMapping: { [key in GroceryCategory]: MaterialIconNames } = {
//за сад се иконице за категорију бирају одавде //за сад се иконице за категорију бирају одавде
[GroceryCategory.Fruit]: "food-apple", [GroceryCategory.Fruit]: "food-apple",
@ -48,15 +50,16 @@ const iconMapping: { [key in GroceryCategory]: MaterialIconNames } = {
[GroceryCategory.Household]: "home", [GroceryCategory.Household]: "home",
[GroceryCategory.PersonalCare]: "face-man-profile", [GroceryCategory.PersonalCare]: "face-man-profile",
[GroceryCategory.Frozen]: "snowflake", [GroceryCategory.Frozen]: "snowflake",
}; };*/
interface IGroceryContext { interface IGroceryContext {
groceries: IGrocery[]; groceries: IGrocery[];
iconMapping: { [key in GroceryCategory]: MaterialIconNames }; //iconMapping: { [key in GroceryCategory]: MaterialIconNames };
updateGroceryItem: (id: number, changes: Partial<IGrocery>) => void; updateGroceryItem: (id: number, changes: Partial<IGrocery>) => void;
isAddingGrocery: boolean; isAddingGrocery: boolean;
setIsAddingGrocery: (value: boolean) => void; setIsAddingGrocery: (value: boolean) => void;
addGrocery: (grocery: IGrocery) => void; addGrocery: (grocery: IGrocery) => void;
fuzzyMatchGroceryCategory: (groceryTitle: string) => GroceryCategory;
} }
const GroceryContext = createContext<IGroceryContext | undefined>(undefined); const GroceryContext = createContext<IGroceryContext | undefined>(undefined);
@ -109,11 +112,114 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
...prevGroceries, ...prevGroceries,
{ {
...grocery, ...grocery,
id: prevGroceries.length ? prevGroceries[prevGroceries.length - 1].id + 1 : 0, id: prevGroceries.length
? prevGroceries[prevGroceries.length - 1].id + 1
: 0,
}, },
]); ]);
}; };
const groceryExamples = {
[GroceryCategory.Fruit]: [
'apple', 'apples', 'banana', 'bananas', 'orange', 'oranges', 'grape', 'grapes',
'pear', 'pears', 'pineapple', 'pineapples', 'kiwi', 'kiwis', 'strawberry',
'strawberries', 'blueberry', 'blueberries', 'mango', 'mangoes', 'watermelon',
'watermelons', 'peach', 'peaches', 'plum', 'plums', 'cherry', 'cherries',
'raspberry', 'raspberries', 'blackberry', 'blackberries', 'pomegranate',
'pomegranates', 'lemon', 'lemons', 'lime', 'limes', 'coconut', 'coconuts'
],
[GroceryCategory.Dairy]: [
'milk', 'whole milk', 'skim milk', 'almond milk', 'soy milk', 'cheese',
'cheeses', 'yoghurt', 'yogurt', 'greek yoghurt', 'greek yogurt', 'butter',
'margarine', 'cream', 'whipping cream', 'heavy cream', 'ice cream', 'ice creams',
'sour cream', 'whipped cream', 'cream cheese', 'cream cheeses', 'buttermilk',
'cottage cheese', 'ghee', 'kefir'
],
[GroceryCategory.Vegetables]: [
'carrot', 'carrots', 'broccoli', 'lettuce', 'lettuces', 'spinach', 'kale',
'cabbage', 'cabbages', 'cauliflower', 'zucchini', 'zucchinis', 'onion', 'onions',
'garlic', 'pepper', 'peppers', 'bell pepper', 'bell peppers', 'tomato', 'tomatoes',
'cucumber', 'cucumbers', 'potato', 'potatoes', 'sweet potato', 'sweet potatoes',
'beet', 'beets', 'eggplant', 'eggplants', 'celery', 'radish', 'radishes',
'asparagus', 'mushroom', 'mushrooms'
],
[GroceryCategory.Meat]: [
'steak', 'steaks', 'beef', 'pork', 'lamb', 'veal', 'ribeye', 'tenderloin',
'filet mignon', 'bacon', 'ham', 'sausage', 'sausages', 'salami', 'ground beef',
'beef mince', 'hamburger meat', 'prosciutto', 'brisket', 'pork chop', 'pork chops'
],
[GroceryCategory.Poultry]: [
'chicken', 'whole chicken', 'chickens', 'chicken breast', 'chicken breasts',
'breast of chicken', 'chicken thigh', 'chicken thighs', 'chicken leg', 'chicken legs',
'chicken wing', 'chicken wings', 'wing', 'wings', 'drumsticks', 'chicken drumstick',
'turkey', 'whole turkey', 'ground turkey', 'turkey breast', 'turkey breasts',
'turkey leg', 'duck', 'ducks', 'goose', 'quail', 'pheasant'
],
[GroceryCategory.Bakery]: [
'bread', 'breads', 'whole wheat bread', 'sourdough bread', 'bagel', 'bagels',
'croissant', 'croissants', 'muffin', 'muffins', 'buns', 'hamburger bun',
'hamburger buns', 'hotdog bun', 'hotdog buns', 'donut', 'donuts', 'roll', 'rolls',
'dinner roll', 'dinner rolls', 'scone', 'scones', 'toast', 'ciabatta',
'focaccia', 'brioche', 'pita', 'pitas', 'naan', 'baguette', 'baguettes',
'pastry', 'pastries', 'pretzel', 'pretzels'
],
[GroceryCategory.Beverages]: [
'coffee', 'decaf coffee', 'iced coffee', 'cold brew', 'tea', 'iced tea',
'black tea', 'green tea', 'herbal tea', 'juice', 'apple juice', 'orange juice',
'grape juice', 'water', 'sparkling water', 'flavored water', 'soda', 'cola',
'diet soda', 'beer', 'lager', 'ale', 'wine', 'red wine', 'white wine',
'whiskey', 'whisky', 'vodka', 'rum', 'gin', 'smoothie', 'smoothies', 'milkshake',
'milkshakes', 'energy drink', 'energy drinks', 'sports drink', 'sports drinks',
'lemonade', 'sparkling lemonade', 'iced lemonade', 'sparkling water', 'cider',
'hard cider', 'kombucha'
],
[GroceryCategory.Snacks]: [
'chips', 'potato chips', 'tortilla chips', 'corn chips', 'candy', 'candies',
'chocolate', 'chocolates', 'dark chocolate', 'milk chocolate', 'white chocolate',
'cookies', 'popcorn', 'pretzel', 'pretzels', 'nuts', 'mixed nuts', 'almonds',
'cashews', 'trail mix', 'granola bar', 'granola bars', 'protein bar', 'protein bars',
'crackers', 'gummies', 'gummy bears', 'fruit snacks', 'dried fruit',
'peanut butter', 'rice cakes', 'snack cakes'
],
[GroceryCategory.Household]: [
'detergent', 'laundry detergent', 'dish soap', 'dishwasher detergent',
'toilet paper', 'paper towels', 'trash bags', 'bin liners', 'broom', 'mop',
'cleaner', 'surface cleaner', 'multi-purpose cleaner', 'disinfectant', 'bleach',
'fabric softener', 'vacuum bags', 'aluminum foil', 'plastic wrap', 'cling wrap',
'light bulbs', 'batteries', 'laundry soap', 'sponges', 'scrubbers',
'dishwashing liquid'
],
[GroceryCategory.PersonalCare]: [
'shampoo', 'conditioner', 'hair shampoo', 'hair conditioner', 'soap', 'bar soap',
'liquid soap', 'toothpaste', 'toothbrush', 'toothbrushes', 'electric toothbrush',
'deodorant', 'antiperspirant', 'lotion', 'moisturizer', 'razor', 'shaving razor',
'shaving cream', 'body wash', 'face wash', 'sunscreen', 'hair gel', 'hair spray',
'nail polish', 'cotton swabs', 'lip balm', 'chapstick', 'hand cream', 'sanitizer'
],
[GroceryCategory.Frozen]: [
'ice cream', 'ice creams', 'frozen pizza', 'frozen pizzas', 'frozen vegetables',
'frozen veg', 'frozen peas', 'frozen fruit', 'frozen fish', 'frozen chicken',
'frozen wings', 'frozen fries', 'frozen french fries', 'frozen shrimp',
'frozen prawns', 'frozen dumplings', 'frozen waffles', 'frozen pancakes',
'frozen pie', 'frozen pies', 'frozen lasagna', 'frozen burrito', 'frozen burritos',
'frozen nuggets', 'frozen pastry', 'frozen pastries', 'frozen meals'
],
};
const fuzzyMatchGroceryCategory = (groceryTitle: string): GroceryCategory => {
let bestMatchCategory = GroceryCategory.None;
let bestMatchScore = -1000;
Object.entries(groceryExamples).forEach(([category, examples]) => {
const matches = fuzzysort.go(groceryTitle, examples);
if (matches.length > 0 && matches[0].score > bestMatchScore) {
bestMatchScore = matches[0].score;
bestMatchCategory = category as GroceryCategory;
}
});
return bestMatchCategory;
};
const updateGroceryItem = (id: number, changes: Partial<IGrocery>) => { const updateGroceryItem = (id: number, changes: Partial<IGrocery>) => {
setGroceries((prevGroceries) => setGroceries((prevGroceries) =>
@ -125,7 +231,15 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
return ( return (
<GroceryContext.Provider <GroceryContext.Provider
value={{ groceries, iconMapping, updateGroceryItem, isAddingGrocery, setIsAddingGrocery, addGrocery }} value={{
groceries,
fuzzyMatchGroceryCategory,
//iconMapping,
updateGroceryItem,
isAddingGrocery,
setIsAddingGrocery,
addGrocery,
}}
> >
{children} {children}
</GroceryContext.Provider> </GroceryContext.Provider>

View File

@ -21,6 +21,7 @@ interface IToDosContext {
toDos: IToDo[]; toDos: IToDo[];
updateToDo: (id: number, changes: Partial<IToDo>) => void; updateToDo: (id: number, changes: Partial<IToDo>) => void;
addToDo: (newToDo: IToDo) => void; addToDo: (newToDo: IToDo) => void;
maxPoints: number;
} }
const ToDosContext = createContext<IToDosContext>(undefined!); const ToDosContext = createContext<IToDosContext>(undefined!);
@ -35,7 +36,7 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: new Date(), date: new Date(),
rotate: true, rotate: true,
repeatType: "Every week" repeatType: "Every week",
}, },
{ {
id: 1, id: 1,
@ -43,7 +44,7 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: new Date(), date: new Date(),
rotate: false, rotate: false,
repeatType: "Every week" repeatType: "Every week",
}, },
{ {
id: 2, id: 2,
@ -51,7 +52,7 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: new Date(), date: new Date(),
rotate: true, rotate: true,
repeatType: "Every week" repeatType: "Every week",
}, },
{ {
id: 3, id: 3,
@ -60,7 +61,7 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
date: new Date(Date.now() + 86400000), date: new Date(Date.now() + 86400000),
points: 40, points: 40,
rotate: false, rotate: false,
repeatType: "Every week" repeatType: "Every week",
}, },
{ {
id: 4, id: 4,
@ -68,7 +69,7 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: new Date(Date.now() + 86400000), date: new Date(Date.now() + 86400000),
rotate: false, rotate: false,
repeatType: "Once a Month" repeatType: "Once a Month",
}, },
{ {
id: 5, id: 5,
@ -76,7 +77,7 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: new Date(Date.now() + 2 * 86400000), date: new Date(Date.now() + 2 * 86400000),
rotate: true, rotate: true,
repeatType: "Once a Month" repeatType: "Once a Month",
}, },
{ {
id: 6, id: 6,
@ -84,7 +85,8 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: new Date(Date.now() + 3 * 86400000), date: new Date(Date.now() + 3 * 86400000),
rotate: false, rotate: false,
repeatType: "Once a year" repeatType: "Once a year",
points: 50,
}, },
{ {
id: 7, id: 7,
@ -92,7 +94,7 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: null, date: null,
rotate: false, rotate: false,
repeatType: "None" repeatType: "None",
}, },
{ {
id: 8, id: 8,
@ -100,14 +102,34 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
done: false, done: false,
date: null, date: null,
rotate: false, rotate: false,
repeatType: "None" repeatType: "None",
points: 10,
}, },
]); ]);
const initCalc = (): number => {
return toDos.reduce(
(sum, todo) => sum + (todo.points ? todo.points : 0),
50
);
};
const calculateMaxPoints = () => {
const totalPoints = toDos.reduce(
(sum, todo) => sum + (todo.points ? todo.points : 0),
0
);
setMaxPoints(totalPoints);
};
const [maxPoints, setMaxPoints] = useState<number>(initCalc);
const updateToDo = (id: number, changes: Partial<IToDo>) => { const updateToDo = (id: number, changes: Partial<IToDo>) => {
setToDos((prevToDos) => setToDos((prevToDos) =>
prevToDos.map((toDo) => (toDo.id === id ? { ...toDo, ...changes } : toDo)) prevToDos.map((toDo) => (toDo.id === id ? { ...toDo, ...changes } : toDo))
); );
calculateMaxPoints();
}; };
const addToDo = (newToDo: IToDo) => { const addToDo = (newToDo: IToDo) => {
@ -118,10 +140,11 @@ export const ToDosContextProvider: FC<{ children: ReactNode }> = ({
id: prevToDos.length ? prevToDos[prevToDos.length - 1].id + 1 : 0, id: prevToDos.length ? prevToDos[prevToDos.length - 1].id + 1 : 0,
}, },
]); ]);
calculateMaxPoints();
}; };
return ( return (
<ToDosContext.Provider value={{ toDos, updateToDo, addToDo }}> <ToDosContext.Provider value={{ toDos, updateToDo, addToDo, maxPoints }}>
{children} {children}
</ToDosContext.Provider> </ToDosContext.Provider>
); );

View File

@ -0,0 +1,30 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
export const useSetUserData = () => {
const {user: currentUser, setProfileData} = useAuthContext()
return useMutation({
mutationKey: ["updateUserData"],
mutationFn: async ({newUserData, customUser}: {newUserData: Partial<UserProfile>, customUser?: FirebaseAuthTypes.User }) => {
const user = currentUser ?? customUser
if (user) {
try {
await firestore()
.collection("Profiles")
.doc(user.uid)
.set(newUserData);
const profileData = await firestore().collection("Profiles").doc(user?.uid!).get()
setProfileData(profileData.data() as UserProfile)
} catch (e) {
console.error(e)
}
}
}
})
}

View File

@ -1,16 +1,40 @@
import {useMutation} from "react-query"; import { useMutation } from "react-query";
import auth from "@react-native-firebase/auth"; import auth from "@react-native-firebase/auth";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData"; import { ProfileType } from "@/contexts/AuthContext";
import {ProfileType} from "@/contexts/AuthContext"; import { useSetUserData } from "./useSetUserData";
export const useSignUp = () => { export const useSignUp = () => {
const {mutateAsync: updateUserData} = useUpdateUserData() const { mutateAsync: setUserData } = useSetUserData();
return useMutation({ return useMutation({
mutationKey: ["signUp"], mutationKey: ["signUp"],
mutationFn: async ({email, password, firstName, lastName}: { email: string, password: string, firstName: string, lastName: string }) => { mutationFn: async ({
const res = await auth().createUserWithEmailAndPassword(email, password); email,
await updateUserData({newUserData: {userType: ProfileType.PARENT, firstName: firstName, lastName: lastName}, customUser: res.user}); password,
} firstName,
}); lastName,
} }: {
email: string;
password: string;
firstName: string;
lastName: string;
}) => {
await auth()
.createUserWithEmailAndPassword(email, password)
.then(async (res) => {
try {
await setUserData({
newUserData: {
userType: ProfileType.PARENT,
firstName: firstName,
lastName: lastName,
},
customUser: res.user,
});
} catch (error) {
console.error(error);
}
});
},
});
};

274
package-lock.json generated
View File

@ -32,6 +32,7 @@
"expo-web-browser": "~13.0.3", "expo-web-browser": "~13.0.3",
"firebase-admin": "^12.3.1", "firebase-admin": "^12.3.1",
"firebase-functions": "^5.1.0", "firebase-functions": "^5.1.0",
"fuzzysort": "^3.0.2",
"jotai": "^2.9.1", "jotai": "^2.9.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@ -2522,9 +2523,9 @@
} }
}, },
"node_modules/@expo/config-plugins": { "node_modules/@expo/config-plugins": {
"version": "8.0.8", "version": "8.0.9",
"resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-8.0.8.tgz", "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-8.0.9.tgz",
"integrity": "sha512-Fvu6IO13EUw0R9WeqxUO37FkM62YJBNcZb9DyJAOgMz7Ez/vaKQGEjKt9cwT+Q6uirtCATMgaq6VWAW7YW8xXw==", "integrity": "sha512-dNCG45C7BbDPV9MdWvCbsFtJtVn4w/TJbb5b7Yr6FA8HYIlaaVM0wqUMzTPmGj54iYXw8X/Vge8uCPxg7RWgeA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@expo/config-types": "^51.0.0-unreleased", "@expo/config-types": "^51.0.0-unreleased",
@ -3449,9 +3450,9 @@
} }
}, },
"node_modules/@expo/vector-icons": { "node_modules/@expo/vector-icons": {
"version": "14.0.2", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.2.tgz", "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.3.tgz",
"integrity": "sha512-70LpmXQu4xa8cMxjp1fydgRPsalefnHaXLzIwaHMEzcZhnyjw2acZz8azRrZOslPVAWlxItOa2Dd7WtD/kI+CA==", "integrity": "sha512-UJAKOXPPi6ez/1QZfoFVopCH3+c12Sw+T+IIVkvONCEN7zjN1fLxxWHkZ7Spz4WO5EH2ObtaJfCe/k4rw+ftxA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
@ -9379,9 +9380,9 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
@ -9392,7 +9393,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@ -11522,24 +11523,24 @@
} }
}, },
"node_modules/expo": { "node_modules/expo": {
"version": "51.0.30", "version": "51.0.34",
"resolved": "https://registry.npmjs.org/expo/-/expo-51.0.30.tgz", "resolved": "https://registry.npmjs.org/expo/-/expo-51.0.34.tgz",
"integrity": "sha512-eo4T2TGnKyDkLryY6lYuGVV1FZyWa1FG7Ns0aqfZ7N/PFQYDfbowVwkcEWbRppSzqSfjGXYqvVV4WZNFbpgZZw==", "integrity": "sha512-l2oi+hIj/ph3qGcvM54Nyd2uF3Zq5caVmSg7AXfBUgtvcdv5Pj1EI/2xCXP9tfMNQo351CWyOwBkTGjv+GdrLg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@expo/cli": "0.18.29", "@expo/cli": "0.18.29",
"@expo/config": "9.0.3", "@expo/config": "9.0.3",
"@expo/config-plugins": "8.0.8", "@expo/config-plugins": "8.0.9",
"@expo/metro-config": "0.18.11", "@expo/metro-config": "0.18.11",
"@expo/vector-icons": "^14.0.0", "@expo/vector-icons": "^14.0.3",
"babel-preset-expo": "~11.0.14", "babel-preset-expo": "~11.0.14",
"expo-asset": "~10.0.10", "expo-asset": "~10.0.10",
"expo-file-system": "~17.0.1", "expo-file-system": "~17.0.1",
"expo-font": "~12.0.9", "expo-font": "~12.0.10",
"expo-keep-awake": "~13.0.2", "expo-keep-awake": "~13.0.2",
"expo-modules-autolinking": "1.11.2", "expo-modules-autolinking": "1.11.2",
"expo-modules-core": "1.12.22", "expo-modules-core": "1.12.24",
"fbemitter": "^3.0.0", "fbemitter": "^3.0.0",
"whatwg-url-without-unicode": "8.0.0-3" "whatwg-url-without-unicode": "8.0.0-3"
}, },
@ -11703,9 +11704,9 @@
} }
}, },
"node_modules/expo-font": { "node_modules/expo-font": {
"version": "12.0.9", "version": "12.0.10",
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-12.0.9.tgz", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-12.0.10.tgz",
"integrity": "sha512-seTCyf0tbgkAnp3ZI9ZfK9QVtURQUgFnuj+GuJ5TSnN0XsOtVe1s2RxTvmMgkfuvfkzcjJ69gyRpsZS1cC8hjw==", "integrity": "sha512-Q1i2NuYri3jy32zdnBaHHCya1wH1yMAsI+3CCmj9zlQzlhsS9Bdwcj2W3c5eU5FvH2hsNQy4O+O1NnM6o/pDaQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fontfaceobserver": "^2.1.0" "fontfaceobserver": "^2.1.0"
@ -11877,9 +11878,9 @@
} }
}, },
"node_modules/expo-modules-core": { "node_modules/expo-modules-core": {
"version": "1.12.22", "version": "1.12.24",
"resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.22.tgz", "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.12.24.tgz",
"integrity": "sha512-MX9qJRuVszyuGksOZ1QkMcUGcAZ1o2AmDigkQAl9yxqFtwEpBOxELs3rXeQol7WiEabvK+bERTF9LtSTDCVCYw==", "integrity": "sha512-3geIe2ecizlp7l26iY8Nmc59z2d1RUC5nQZtI9iJoi5uHEUV/zut8e4zRLFVnZb8KOcMcEDsrvaBL5DPnqdfpg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"invariant": "^2.2.4" "invariant": "^2.2.4"
@ -12055,37 +12056,37 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@ -12111,14 +12112,23 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/express/node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/finalhandler": { "node_modules/express/node_modules/finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@ -12129,6 +12139,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/express/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/express/node_modules/ms": { "node_modules/express/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -12147,6 +12169,45 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/express/node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express/node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/express/node_modules/statuses": { "node_modules/express/node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -12832,6 +12893,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fuzzysort": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.0.2.tgz",
"integrity": "sha512-ZyahVgxvckB1Qosn7YGWLDJJp2XlyaQ2WmZeI+d0AzW0AMqVYnz5N89G6KAKa6m/LOtv+kzJn4lhDF/yVg11Cg==",
"license": "MIT"
},
"node_modules/gaxios": { "node_modules/gaxios": {
"version": "6.7.1", "version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
@ -17495,10 +17562,13 @@
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT" "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@ -18906,9 +18976,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {
@ -19300,12 +19370,12 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -20631,20 +20701,116 @@
} }
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/serve-static/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/serve-static/node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/serve-static/node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static/node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static/node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-static/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/set-blocking": { "node_modules/set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",

View File

@ -39,6 +39,7 @@
"expo-web-browser": "~13.0.3", "expo-web-browser": "~13.0.3",
"firebase-admin": "^12.3.1", "firebase-admin": "^12.3.1",
"firebase-functions": "^5.1.0", "firebase-functions": "^5.1.0",
"fuzzysort": "^3.0.2",
"jotai": "^2.9.1", "jotai": "^2.9.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",

1408
yarn.lock

File diff suppressed because it is too large Load Diff