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,
Text,
Button,
TouchableOpacity,
Checkbox,
ButtonSize,
} from "react-native-ui-lib";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext"; import {AntDesign} from "@expo/vector-icons";
import { MaterialCommunityIcons, AntDesign } from "@expo/vector-icons"; import {GroceryCategory, useGroceryContext,} from "@/contexts/GroceryContext";
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,19 +82,7 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
recurring: false, recurring: false,
frequency: GroceryFrequency.Never, frequency: GroceryFrequency.Never,
}, },
]); ];
const addGrocery = (grocery: IGrocery) => {
setGroceries((prevGroceries) => [
...prevGroceries,
{
...grocery,
id: prevGroceries.length
? prevGroceries[prevGroceries.length - 1].id + 1
: 0,
},
]);
};
const groceryExamples = { const groceryExamples = {
[GroceryCategory.Fruit]: [ [GroceryCategory.Fruit]: [
@ -206,6 +171,32 @@ export const GroceryProvider: React.FC<{ children: React.ReactNode }> = ({
], ],
}; };
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 => {
let bestMatchCategory = GroceryCategory.None; let bestMatchCategory = GroceryCategory.None;
let bestMatchScore = -1000; let bestMatchScore = -1000;
@ -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")
}
})
}