Shopping List backend implementation

- Implemented fetching, create and update of groceries in db
This commit is contained in:
Dejan
2024-10-11 16:11:46 +02:00
parent a05de1b333
commit 9b94aa8e70
8 changed files with 272 additions and 208 deletions

View File

@ -1,5 +1,5 @@
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import React, { useState } from "react"; import React from "react";
import { import {
Dialog, Dialog,
Text, Text,
@ -11,9 +11,9 @@ import {
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import { import {
GroceryFrequency, GroceryFrequency,
IGrocery,
useGroceryContext, useGroceryContext,
} from "@/contexts/GroceryContext"; } from "@/contexts/GroceryContext";
import { IGrocery } from "@/hooks/firebase/types/groceryData";
interface EditGroceryFrequencyProps { interface EditGroceryFrequencyProps {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
@ -42,7 +42,7 @@ const EditGroceryFrequency = (props: EditGroceryFrequencyProps) => {
<Switch <Switch
value={props.item.recurring} value={props.item.recurring}
onValueChange={(value) => onValueChange={(value) =>
updateGroceryItem(props.item.id, { recurring: value }) updateGroceryItem({id: props.item.id, recurring: value})
} }
onColor={"lime"} onColor={"lime"}
/> />
@ -56,7 +56,8 @@ const EditGroceryFrequency = (props: EditGroceryFrequencyProps) => {
const selectedFrequency = const selectedFrequency =
GroceryFrequency[item as keyof typeof GroceryFrequency]; GroceryFrequency[item as keyof typeof GroceryFrequency];
if (selectedFrequency) { if (selectedFrequency) {
updateGroceryItem(props.item.id, { updateGroceryItem({
id: props.item.id,
frequency: selectedFrequency, frequency: selectedFrequency,
}); });
} else { } else {

View File

@ -1,34 +1,21 @@
import { import {Checkbox, Text, TouchableOpacity, View,} from "react-native-ui-lib";
View, import React, {useEffect, useState} from "react";
Text, import {AntDesign} from "@expo/vector-icons";
Button, import {GroceryCategory, useGroceryContext,} from "@/contexts/GroceryContext";
TouchableOpacity,
Checkbox,
ButtonSize,
} from "react-native-ui-lib";
import React, { useEffect, useState } from "react";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { MaterialCommunityIcons, AntDesign } from "@expo/vector-icons";
import { ListItem } from "react-native-ui-lib";
import {
GroceryCategory,
IGrocery,
useGroceryContext,
} from "@/contexts/GroceryContext";
import EditGroceryFrequency from "./EditGroceryFrequency"; import EditGroceryFrequency from "./EditGroceryFrequency";
import EditGroceryItem from "./EditGroceryItem"; import EditGroceryItem from "./EditGroceryItem";
import { StyleSheet } from "react-native"; import {StyleSheet} from "react-native";
import {IGrocery} from "@/hooks/firebase/types/groceryData";
const GroceryItem = ({ const GroceryItem = ({
item, item,
handleItemApproved, handleItemApproved,
}: { }: {
item: IGrocery; item: IGrocery;
handleItemApproved: (id: number, changes: Partial<IGrocery>) => void; handleItemApproved: (id: string, changes: Partial<IGrocery>) => void;
}) => { }) => {
const { updateGroceryItem, groceries } = useGroceryContext(); const { updateGroceryItem } = useGroceryContext();
const { profileType } = useAuthContext();
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>("");
@ -37,11 +24,11 @@ const GroceryItem = ({
); );
const handleTitleChange = (newTitle: string) => { const handleTitleChange = (newTitle: string) => {
updateGroceryItem(item.id, { title: newTitle }); updateGroceryItem({id: item?.id, title: newTitle});
}; };
const handleCategoryChange = (newCategory: GroceryCategory) => { const handleCategoryChange = (newCategory: GroceryCategory) => {
updateGroceryItem(item.id, { category: newCategory }); updateGroceryItem({id: item?.id, category: newCategory});
}; };
useEffect(() => { useEffect(() => {
@ -114,7 +101,7 @@ const GroceryItem = ({
containerStyle={styles.checkbox} containerStyle={styles.checkbox}
hitSlop={20} hitSlop={20}
onValueChange={() => onValueChange={() =>
updateGroceryItem(item.id, { bought: !item.bought }) updateGroceryItem({ id: item.id, bought: !item.bought })
} }
/> />
)} )}

View File

@ -1,18 +1,13 @@
import { FlatList, StyleSheet } from "react-native"; import {FlatList, StyleSheet} from "react-native";
import React, { RefObject, useEffect, useState } from "react"; import React, {useEffect, useState} from "react";
import { View, Text, ListItem, Button, TextField, TextFieldRef } from "react-native-ui-lib"; import {Button, Text, View} from "react-native-ui-lib";
import GroceryItem from "./GroceryItem"; import GroceryItem from "./GroceryItem";
import { import {GroceryCategory, GroceryFrequency, useGroceryContext,} from "@/contexts/GroceryContext";
IGrocery,
GroceryCategory,
GroceryFrequency,
useGroceryContext,
} from "@/contexts/GroceryContext";
import HeaderTemplate from "@/components/shared/HeaderTemplate"; import HeaderTemplate from "@/components/shared/HeaderTemplate";
import CategoryDropdown from "./CategoryDropdown"; import {AntDesign, MaterialIcons} from "@expo/vector-icons";
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import EditGroceryItem from "./EditGroceryItem"; import EditGroceryItem from "./EditGroceryItem";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext"; import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {IGrocery} from "@/hooks/firebase/types/groceryData";
const GroceryList = () => { const GroceryList = () => {
const { const {
@ -24,10 +19,10 @@ const GroceryList = () => {
} = useGroceryContext(); } = useGroceryContext();
const { profileData } = useAuthContext(); const { profileData } = useAuthContext();
const [approvedGroceries, setapprovedGroceries] = useState<IGrocery[]>( const [approvedGroceries, setapprovedGroceries] = useState<IGrocery[]>(
groceries.filter((item) => item.approved === true) groceries?.filter((item) => item.approved === true)
); );
const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>( const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>(
groceries.filter((item) => item.approved !== true) groceries?.filter((item) => item.approved !== true)
); );
const [category, setCategory] = useState<GroceryCategory>( const [category, setCategory] = useState<GroceryCategory>(
GroceryCategory.Bakery GroceryCategory.Bakery
@ -39,7 +34,7 @@ const GroceryList = () => {
const [approvedVisible, setApprovedVisible] = useState<boolean>(true); const [approvedVisible, setApprovedVisible] = useState<boolean>(true);
// Group approved groceries by category // Group approved groceries by category
const approvedGroceriesByCategory = approvedGroceries.reduce( const approvedGroceriesByCategory = approvedGroceries?.reduce(
(groups: any, item: IGrocery) => { (groups: any, item: IGrocery) => {
const category = item.category || "Uncategorized"; const category = item.category || "Uncategorized";
if (!groups[category]) { if (!groups[category]) {
@ -76,8 +71,8 @@ const GroceryList = () => {
}, [category]); }, [category]);
useEffect(() => { useEffect(() => {
setapprovedGroceries(groceries.filter((item) => item.approved === true)); setapprovedGroceries(groceries?.filter((item) => item.approved === true));
setPendingGroceries(groceries.filter((item) => item.approved !== true)); setPendingGroceries(groceries?.filter((item) => item.approved !== true));
}, [groceries]); }, [groceries]);
return ( return (
@ -93,8 +88,8 @@ const GroceryList = () => {
style={{ borderRadius: 50 }} style={{ borderRadius: 50 }}
> >
<Text text70BL color="#46a80a"> <Text text70BL color="#46a80a">
{approvedGroceries.length} list{" "} {approvedGroceries?.length} list{" "}
{approvedGroceries.length === 1 ? ( {approvedGroceries?.length === 1 ? (
<Text text70BL color="#46a80a"> <Text text70BL color="#46a80a">
item item
</Text> </Text>
@ -111,7 +106,7 @@ const GroceryList = () => {
style={{ borderRadius: 50 }} style={{ borderRadius: 50 }}
> >
<Text text70BL color="#e28800"> <Text text70BL color="#e28800">
{pendingGroceries.length} pending {pendingGroceries?.length} pending
</Text> </Text>
</View> </View>
<Button <Button
@ -161,18 +156,18 @@ const GroceryList = () => {
}} }}
> >
<Text text70 center color="#e28800"> <Text text70 center color="#e28800">
{pendingGroceries.length.toString()} {pendingGroceries?.length.toString()}
</Text> </Text>
</View> </View>
</View> </View>
{pendingGroceries.length > 0 {pendingGroceries?.length > 0
? pendingVisible && ( ? pendingVisible && (
<FlatList <FlatList
data={pendingGroceries} data={pendingGroceries}
renderItem={({ item }) => ( renderItem={({ item }) => (
<GroceryItem <GroceryItem
item={item} item={item}
handleItemApproved={updateGroceryItem} handleItemApproved={(id, changes) => updateGroceryItem({...changes, id: id})}
/> />
)} )}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
@ -217,7 +212,7 @@ const GroceryList = () => {
}} }}
> >
<Text text70 center color="#46a80a"> <Text text70 center color="#46a80a">
{approvedGroceries.length.toString()} {approvedGroceries?.length.toString()}
</Text> </Text>
</View> </View>
</View> </View>
@ -234,7 +229,7 @@ const GroceryList = () => {
)} )}
{/* Render Approved Groceries Grouped by Category */} {/* Render Approved Groceries Grouped by Category */}
{approvedGroceries.length > 0 {approvedGroceries?.length > 0
? approvedVisible && ( ? approvedVisible && (
<FlatList <FlatList
data={Object.keys(approvedGroceriesByCategory)} data={Object.keys(approvedGroceriesByCategory)}
@ -250,7 +245,7 @@ const GroceryList = () => {
<GroceryItem <GroceryItem
key={grocery.id} key={grocery.id}
item={grocery} item={grocery}
handleItemApproved={updateGroceryItem} handleItemApproved={(id, changes) => updateGroceryItem({...changes, id: id})}
/> />
) )
)} )}

View File

@ -1,6 +1,9 @@
import { MaterialCommunityIcons } from "@expo/vector-icons"; import {createContext, useContext, useState} from "react";
import { createContext, useContext, useState } from "react";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import {useCreateGrocery} from "@/hooks/firebase/useCreateGrocery";
import {IGrocery} from "@/hooks/firebase/types/groceryData";
import {useUpdateGrocery} from "@/hooks/firebase/useUpdateGrocery";
import {useGetGroceries} from "@/hooks/firebase/useGetGroceries";
export enum GroceryFrequency { export enum GroceryFrequency {
Never = "Never", Never = "Never",
@ -11,16 +14,6 @@ export enum GroceryFrequency {
Quarterly = "Quarterly", Quarterly = "Quarterly",
} }
export interface IGrocery {
id: number;
title: string;
category: GroceryCategory;
approved: boolean;
recurring: boolean;
frequency: GroceryFrequency;
bought: boolean;
}
export enum GroceryCategory { export enum GroceryCategory {
Fruit = "Fruit", Fruit = "Fruit",
Dairy = "Dairy", Dairy = "Dairy",
@ -52,23 +45,7 @@ const iconMapping: { [key in GroceryCategory]: MaterialIconNames } = {
[GroceryCategory.Frozen]: "snowflake", [GroceryCategory.Frozen]: "snowflake",
};*/ };*/
interface IGroceryContext { const initialGroceryList = [
groceries: IGrocery[];
//iconMapping: { [key in GroceryCategory]: MaterialIconNames };
updateGroceryItem: (id: number, changes: Partial<IGrocery>) => void;
isAddingGrocery: boolean;
setIsAddingGrocery: (value: boolean) => void;
addGrocery: (grocery: IGrocery) => void;
fuzzyMatchGroceryCategory: (groceryTitle: string) => GroceryCategory;
}
const GroceryContext = createContext<IGroceryContext | undefined>(undefined);
export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isAddingGrocery, setIsAddingGrocery] = useState<boolean>(false);
const [groceries, setGroceries] = useState<IGrocery[]>([
{ {
id: 0, id: 0,
title: "Carrots", title: "Carrots",
@ -105,21 +82,9 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
recurring: false, recurring: false,
frequency: GroceryFrequency.Never, frequency: GroceryFrequency.Never,
}, },
]); ];
const addGrocery = (grocery: IGrocery) => { const groceryExamples = {
setGroceries((prevGroceries) => [
...prevGroceries,
{
...grocery,
id: prevGroceries.length
? prevGroceries[prevGroceries.length - 1].id + 1
: 0,
},
]);
};
const groceryExamples = {
[GroceryCategory.Fruit]: [ [GroceryCategory.Fruit]: [
'apple', 'apples', 'banana', 'bananas', 'orange', 'oranges', 'grape', 'grapes', 'apple', 'apples', 'banana', 'bananas', 'orange', 'oranges', 'grape', 'grapes',
'pear', 'pears', 'pineapple', 'pineapples', 'kiwi', 'kiwis', 'strawberry', 'pear', 'pears', 'pineapple', 'pineapples', 'kiwi', 'kiwis', 'strawberry',
@ -204,6 +169,32 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
'frozen pie', 'frozen pies', 'frozen lasagna', 'frozen burrito', 'frozen burritos', 'frozen pie', 'frozen pies', 'frozen lasagna', 'frozen burrito', 'frozen burritos',
'frozen nuggets', 'frozen pastry', 'frozen pastries', 'frozen meals' 'frozen nuggets', 'frozen pastry', 'frozen pastries', 'frozen meals'
], ],
};
interface IGroceryContext {
groceries: IGrocery[];
//iconMapping: { [key in GroceryCategory]: MaterialIconNames };
updateGroceryItem: (changes: Partial<IGrocery>) => void;
isAddingGrocery: boolean;
setIsAddingGrocery: (value: boolean) => void;
addGrocery: (grocery: IGrocery) => void;
fuzzyMatchGroceryCategory: (groceryTitle: string) => GroceryCategory;
}
const GroceryContext = createContext<IGroceryContext | undefined>(undefined);
export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isAddingGrocery, setIsAddingGrocery] = useState<boolean>(false);
const { mutateAsync: createGrocery } = useCreateGrocery();
const { mutateAsync: updateGrocery } = useUpdateGrocery();
const { data: groceries } = useGetGroceries();
const addGrocery = (grocery: IGrocery) => {
createGrocery(grocery);
}; };
const fuzzyMatchGroceryCategory = (groceryTitle: string): GroceryCategory => { const fuzzyMatchGroceryCategory = (groceryTitle: string): GroceryCategory => {
@ -221,12 +212,8 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
return bestMatchCategory; return bestMatchCategory;
}; };
const updateGroceryItem = (id: number, changes: Partial<IGrocery>) => { const updateGroceryItem = (changes: Partial<IGrocery>) => {
setGroceries((prevGroceries) => updateGrocery(changes);
prevGroceries.map((grocery) =>
grocery.id === id ? { ...grocery, ...changes } : grocery
)
);
}; };
return ( return (

View File

@ -0,0 +1,12 @@
import {GroceryCategory, GroceryFrequency} from "@/contexts/GroceryContext";
export interface IGrocery {
id: string;
title: string;
category: GroceryCategory;
approved: boolean;
recurring: boolean;
frequency: GroceryFrequency;
bought: boolean;
familyId?: string,
}

View File

@ -0,0 +1,26 @@
import { useMutation, useQueryClient } from "react-query";
import firestore from "@react-native-firebase/firestore";
import { useAuthContext } from "@/contexts/AuthContext";
import {IGrocery} from "@/hooks/firebase/types/groceryData";
export const useCreateGrocery = () => {
const { profileData } = useAuthContext();
const queryClients = useQueryClient();
return useMutation({
mutationKey: ["createGrocery"],
mutationFn: async (groceryData: Partial<IGrocery>) => {
try {
const newDoc = firestore().collection('Groceries').doc();
await firestore()
.collection("Groceries")
.add({...groceryData, id: newDoc.id, familyId: profileData?.familyId})
} catch (e) {
console.error(e)
}
},
onSuccess: () => {
queryClients.invalidateQueries("groceries")
}
})
}

View File

@ -0,0 +1,31 @@
import {useQuery} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";
export const useGetGroceries = () => {
const { user, profileData } = useAuthContext();
return useQuery({
queryKey: ["groceries", user?.uid],
queryFn: async () => {
const snapshot = await firestore()
.collection("Groceries")
.where("familyId", "==", profileData?.familyId)
.get();
return snapshot.docs.map((doc) => {
const data = doc.data();
return {
id: doc.id,
title: data.title,
category: data.category,
approved: data.approved,
bought: data.bought,
recurring: data.recurring,
frequency: data.frequency
};
});
}
})
};

View File

@ -0,0 +1,25 @@
import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {IGrocery} from "@/hooks/firebase/types/groceryData";
export const useUpdateGrocery = () => {
const queryClients = useQueryClient()
return useMutation({
mutationKey: ["updateGrocery"],
mutationFn: async (groceryData: Partial<IGrocery>) => {
console.log(groceryData);
try {
await firestore()
.collection("Groceries")
.doc(groceryData.id)
.update(groceryData);
} catch (e) {
console.error(e)
}
},
onSuccess: () => {
queryClients.invalidateQueries("events")
}
})
}