4 Commits

19 changed files with 2614 additions and 717 deletions

View File

@ -0,0 +1,87 @@
import { SafeAreaView } from "react-native-safe-area-context";
import { Button, Text, View, TextField } from "react-native-ui-lib";
import React, { useState } from "react";
import { useRouter } from "expo-router";
import { StyleSheet } from "react-native";
import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateHouseholdName } from "@/hooks/firebase/useUpdateHouseholdName";
export default function NewHouseholdScreen() {
const router = useRouter();
const { user, profileData } = useAuthContext();
const [householdName, setHouseholdName] = useState("");
const { mutateAsync: newHousehold } = useUpdateHouseholdName();
const handleContinue = async () => {
try {
if(profileData?.familyId)
newHousehold({familyId: profileData?.familyId, name: householdName}).then(() => router.push("/(unauth)/cal_sync"));
} catch (error) {
console.error("Error saving household name:", error);
}
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View
style={{
flex: 1,
padding: 21,
paddingBottom: 45,
paddingTop: "20%",
alignItems: "center",
}}
>
<View gap-13 width={"100%"} marginB-20>
<Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
Name your household
</Text>
<Text color={"#919191"} style={{ fontSize: 20 }}>
Give your family group a unique name!
</Text>
</View>
<View width={"100%"} flexG>
<TextField
value={householdName}
onChangeText={setHouseholdName}
placeholder="Enter household name"
style={styles.textfield}
textAlign="center"
/>
</View>
<View flexG />
<View width={"100%"}>
<Button
label="Continue"
onPress={handleContinue}
style={{
height: 50,
}}
backgroundColor="#fd1775"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
/>
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
textfield: {
backgroundColor: "white",
marginVertical: 100,
padding: 30,
height: 44,
borderRadius: 50,
fontFamily: "PlusJakartaSans_300Light",
fontSize: 15,
color: "#919191",
alignContent: "center",
},
});

View File

@ -57,6 +57,8 @@ import KeyboardManager from 'react-native-keyboard-manager';
import {enableScreens} from 'react-native-screens';
import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider";
import auth from "@react-native-firebase/auth";
import firestore from '@react-native-firebase/firestore';
import functions from '@react-native-firebase/functions';
enableScreens(true)
@ -68,9 +70,9 @@ if (Platform.OS === 'ios') {
}
if (__DEV__) {
// functions().useEmulator("localhost", 5001);
// firestore().useEmulator("localhost", 5471);
// auth().useEmulator("http://localhost:9099");
functions().useEmulator("localhost", 5001);
firestore().useEmulator("localhost", 5471);
auth().useEmulator("http://localhost:9099");
}
type TextStyleBase =

View File

@ -13,8 +13,8 @@ interface IAddBrainDumpProps {
}
const AddBrainDump = ({
addBrainDumpProps,
}: {
addBrainDumpProps,
}: {
addBrainDumpProps: IAddBrainDumpProps;
}) => {
const {addBrainDump} = useBrainDumpContext();
@ -22,11 +22,11 @@ const AddBrainDump = ({
const [dumpDesc, setDumpDesc] = useState<string>("");
const {width} = Dimensions.get("screen");
// Refs for the two TextFields
const descriptionRef = useRef<TextFieldRef>(null);
const titleRef = useRef<TextFieldRef>(null);
const isTitleValid = dumpTitle.trim().length >= 3;
useEffect(() => {
setDumpDesc("");
setDumpTitle("");
@ -40,9 +40,9 @@ const AddBrainDump = ({
}
}, [addBrainDumpProps.isVisible]);
useEffect(() => {
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
}, []);
useEffect(() => {
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
}, []);
return (
<Dialog
@ -69,18 +69,17 @@ const AddBrainDump = ({
<Button
color="#05a8b6"
label="Save"
style={styles.topBtn}
style={[styles.topBtn, !isTitleValid && styles.disabledBtn]}
disabled={!isTitleValid}
onPress={() => {
addBrainDump({
id: '99',
title: dumpTitle.trimEnd().trimStart(),
description: dumpDesc.trimEnd().trimStart(),
});
addBrainDumpProps.setIsVisible(false);
if (isTitleValid) {
addBrainDump({
id: '99',
title: dumpTitle.trim(),
description: dumpDesc.trim(),
});
addBrainDumpProps.setIsVisible(false);
}
}}
/>
</View>
@ -94,11 +93,10 @@ const AddBrainDump = ({
setDumpTitle(text);
}}
onSubmitEditing={() => {
// Move focus to the description field
descriptionRef.current?.focus();
}}
style={styles.title}
blurOnSubmit={false} // Keep the keyboard open when moving focus
blurOnSubmit={false}
returnKeyType="next"
/>
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20/>
@ -125,28 +123,31 @@ const AddBrainDump = ({
};
const styles = StyleSheet.create({
dialogContainer: {
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: "white",
padding: 0,
paddingTop: 3,
margin: 0,
},
topBtns: {},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
title: {
fontSize: 22,
fontFamily: "Manrope_500Medium",
},
description: {
fontFamily: "Manrope_400Regular",
fontSize: 14,
textAlignVertical: "top",
},
dialogContainer: {
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: "white",
padding: 0,
paddingTop: 3,
margin: 0,
},
topBtns: {},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
disabledBtn: {
opacity: 0.2,
},
title: {
fontSize: 22,
fontFamily: "Manrope_500Medium",
},
description: {
fontFamily: "Manrope_400Regular",
fontSize: 14,
textAlignVertical: "top",
},
});
export default AddBrainDump;
export default AddBrainDump;

View File

@ -20,14 +20,10 @@ const DumpList = (props: { searchText: string }) => {
return (
<View marginB-70>
{brainDumps?.length ? <FlatList
style={{ zIndex: -1 }}
data={sortedDumps}
keyExtractor={(item) => item.title}
renderItem={({ item }) => (
<BrainDumpItem key={item.title} item={item} />
)}
/> : <Text marginT-20 center style={styles.alert}>You have no notes</Text>}
{sortedDumps?.length ? (
sortedDumps.map((item) => (
<BrainDumpItem key={item.id} item={item} />
))) : <Text marginT-20 center style={styles.alert}>You have no notes</Text>}
</View>
);
};

View File

@ -32,8 +32,11 @@ const AddFeedback = ({
const descriptionRef = useRef<TextFieldRef>(null);
const titleRef = useRef<TextFieldRef>(null);
const isTitleValid = feedbackTitle.trim().length >= 3;
useEffect(() => {
setFeedback("");
setFeedbackTitle("");
}, [addFeedbackProps.isVisible]);
useEffect(() => {
@ -46,9 +49,8 @@ const AddFeedback = ({
useEffect(() => {
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
setFeedbackTitle("");
setFeedback("");
setFeedbackTitle('');
setFeedback('');
}, []);
return (
@ -76,16 +78,17 @@ const AddFeedback = ({
<Button
color="#05a8b6"
label="Save"
style={styles.topBtn}
style={[styles.topBtn, !isTitleValid && styles.disabledBtn]}
disabled={!isTitleValid}
onPress={() => {
addFeedback({
id: 99,
title: feedbackTitle.trimEnd().trimStart(),
text: feedback.trimEnd().trimStart(),
});
addFeedbackProps.setIsVisible(false);
if (isTitleValid) {
addFeedback({
id: 99,
title: feedbackTitle.trim(),
text: feedback.trim(),
});
addFeedbackProps.setIsVisible(false);
}
}}
/>
</View>
@ -142,6 +145,9 @@ const styles = StyleSheet.create({
backgroundColor: "white",
color: "#05a8b6",
},
disabledBtn: {
opacity: 0.2,
},
title: {
fontSize: 22,
fontFamily: "Manrope_500Medium",
@ -153,4 +159,4 @@ const styles = StyleSheet.create({
},
});
export default AddFeedback;
export default AddFeedback;

View File

@ -10,7 +10,7 @@ const Feedback = (props: { item: IFeedback }) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
return (
<View>
<View key={props.item.id}>
<TouchableWithoutFeedback onPress={() => setIsVisible(true)}>
<View
backgroundColor="white"

View File

@ -1,13 +1,13 @@
import { View } from "react-native-ui-lib";
import { View, Text } from "react-native-ui-lib";
import React from "react";
import { FlatList } from "react-native";
import { StyleSheet } from "react-native";
import { useFeedbackContext } from "@/contexts/FeedbackContext";
import Feedback from "./Feedback";
const FeedbackList = (props: { searchText: string }) => {
const { feedbacks } = useFeedbackContext();
const filteredBrainDumps =
const filteredFeedbacks =
props.searchText.trim() === ""
? feedbacks
: feedbacks.filter(
@ -20,16 +20,24 @@ const FeedbackList = (props: { searchText: string }) => {
return (
<View marginB-70>
<FlatList
style={{ zIndex: -1 }}
data={filteredBrainDumps}
keyExtractor={(item) => item.title}
renderItem={({ item }) => (
<Feedback key={item.title} item={item} />
)}
/>
{filteredFeedbacks?.length ? (
filteredFeedbacks.map((item) => (
<Feedback key={item.id} item={item} />
))
) : (
<Text marginT-20 center style={styles.alert}>
You have no Feedbacks
</Text>
)}
</View>
);
};
export default FeedbackList;
const styles = StyleSheet.create({
alert: {
fontFamily: "PlusJakartaSans_300Light",
fontSize: 20
}
});
export default FeedbackList;

View File

@ -165,21 +165,18 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
</View>
</View>
{pendingGroceries?.length > 0
? pendingVisible && (
<FlatList
data={pendingGroceries}
renderItem={({item}) => (
<GroceryItem
item={item}
handleItemApproved={(id, changes) =>
updateGroceryItem({...changes, id: id})
}
onInputFocus={onInputFocus}
/>
)}
keyExtractor={(item) => item.id.toString()}
/>
)
? pendingVisible && (
pendingGroceries.map((item) => (
<GroceryItem
key={item.id.toString()}
item={item}
handleItemApproved={(id, changes) =>
updateGroceryItem({...changes, id: id})
}
onInputFocus={onInputFocus}
/>
))
)
: pendingVisible && (
<Text style={styles.noItemTxt}>No items pending approval.</Text>
)}
@ -230,38 +227,34 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
{/* Render Approved Groceries Grouped by Category */}
{approvedGroceries?.length > 0
? approvedVisible && (
<FlatList
data={Object.keys(approvedGroceriesByCategory).sort((a, b) => {
Object.keys(approvedGroceriesByCategory)
.sort((a, b) => {
if (a !== "Done") return -1;
if (b === "Done") return 1;
return 0;
})}
renderItem={({item: category}) => (
})
.map((category) => (
<View key={category}>
{/* Render Category Header */}
<Text text80M style={{marginTop: 10}} color="#666">
{category}
</Text>
{/* Render Grocery Items for this Category */}
{approvedGroceriesByCategory[category].map(
(grocery: IGrocery) => (
<GroceryItem
key={grocery.id}
item={grocery}
handleItemApproved={(id, changes) =>
updateGroceryItem({...changes, id: id})
}
onInputFocus={onInputFocus}
approvedGroceries={approvedGroceries}
setApprovedGroceries={setApprovedGroceries}
/>
)
)}
{/* Render Category Header */}
<Text text80M style={{marginTop: 10}} color="#666">
{category}
</Text>
{/* Render Grocery Items for this Category */}
{approvedGroceriesByCategory[category].map((grocery: IGrocery) => (
<GroceryItem
key={grocery.id}
item={grocery}
handleItemApproved={(id, changes) =>
updateGroceryItem({...changes, id: id})
}
onInputFocus={onInputFocus}
approvedGroceries={approvedGroceries}
setApprovedGroceries={setApprovedGroceries}
/>
))}
</View>
)}
keyExtractor={(category) => category}
/>
)
))
)
: approvedVisible && (
<Text style={styles.noItemTxt}>No approved items.</Text>
)}

View File

@ -41,14 +41,9 @@ const GroceryWrapper = () => {
const handleInputFocus = (y: number) => {
if (scrollViewRef.current) {
// Get the window height
const windowHeight = Dimensions.get('window').height;
// Calculate the space we want to leave at the top
const topSpacing = 20;
// Calculate the target scroll position:
// y (position of input) - topSpacing (space we want at top)
// if keyboard is shown, we need to account for its height
const scrollPosition = Math.max(0, y - topSpacing);
scrollViewRef.current.scrollTo({

View File

@ -22,14 +22,14 @@ const DeleteProfileDialogs: React.FC<ConfirmationDialogProps> = ({
}) => {
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false);
const [input, setInput] = useState<string>("");
const [isCorrect, setIsCorrect] = useState<boolean>(true);
const [isCorrect, setIsCorrect] = useState<boolean>(false);
useEffect(() => {
setInput("");
}, [onDismiss, onConfirm])
useEffect(() => {
setIsCorrect(input === householdName);
setIsCorrect(input !== "" && input === householdName);
}, [input])

View File

@ -174,7 +174,8 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
updateToDo({
...todo,
points: points,
assignees: selectedAssignees
assignees: selectedAssignees,
currentAssignee: selectedAssignees[0],
});
} else {
return;
@ -186,6 +187,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
done: false,
points: points,
assignees: selectedAssignees,
currentAssignee: selectedAssignees[0],
repeatDays: todo.repeatDays ?? []
});
} else {

View File

@ -231,6 +231,13 @@ const ToDoItem = (props: {
</View>
<View row style={{ gap: 3 }}>
{selectedMembers?.map((member) => {
let currentAssignee = props?.item?.currentAssignee;
let opacity = 1;
if (selectedMembers?.length > 1 && currentAssignee !== member?.uid) {
opacity = 0.4;
}
return member?.pfp ? (
<ImageBackground
key={member?.uid}
@ -242,10 +249,12 @@ const ToDoItem = (props: {
overflow: "hidden",
borderWidth: 2,
borderColor: member.eventColor || "transparent",
opacity: opacity
}}
/>
) : (
<View
key={member.uid}
style={{
position: "relative",
width: 24.64,
@ -253,6 +262,7 @@ const ToDoItem = (props: {
borderWidth: 2,
borderRadius: 100,
borderColor: member.eventColor || "#ccc",
opacity: opacity
}}
>
<View
@ -260,7 +270,7 @@ const ToDoItem = (props: {
backgroundColor: member.eventColor || "#ccc",
justifyContent: "center",
alignItems: "center",
borderRadius: 100, // Circular shape
borderRadius: 100,
width: "100%",
height: "100%",
}}

View File

@ -1813,7 +1813,8 @@ exports.updateHouseholdTimestampOnEventUpdate = functions.firestore
'BrainDumps',
'Groceries',
'Todos',
'Events'
'Events',
//'Feedbacks'
];
for (const collectionName of collections) {
@ -1829,13 +1830,16 @@ exports.updateHouseholdTimestampOnEventUpdate = functions.firestore
batch.delete(profile.ref);
}
const householdDoc = await db.collection('Households')
.doc(familyId)
.get();
if (householdDoc.exists) {
batch.delete(householdDoc.ref);
}
const householdSnapshot = await db.collection('Households')
.where('familyId', '==', familyId)
.get();
if (!householdSnapshot.empty) {
const householdDoc = householdSnapshot.docs[0];
batch.delete(householdDoc.ref);
} else {
console.log('Household not found for familyId:', familyId);
}
await batch.commit();

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ export interface IToDo {
creatorId?: string;
familyId?: string;
assignees?: string[]; // Optional list of assignees
currentAssignee?: string,
connectedTodoId?: string;
}

View File

@ -130,7 +130,7 @@ export const useCreateTodo = () => {
let assignee;
if (todoData.assignees && todoData.rotate && todoData?.assignees?.length !== 0) {
assignee = todoData.assignees[index % todoData.assignees.length];
assignee = todoData.assignees[(index + 1) % todoData.assignees.length];
}
const nextTodo = {
@ -140,7 +140,8 @@ export const useCreateTodo = () => {
familyId: profileData?.familyId,
creatorId: currentUser?.uid,
connectedTodoId: ruleDocRef.id,
assignees: assignee ? [assignee] : todoData.assignees
assignees: todoData.assignees,
currentAssignee: assignee
}
batch.set(newDocRef, nextTodo)

View File

@ -2,9 +2,11 @@ import { useAuthContext } from "@/contexts/AuthContext";
import { useMutation } from "@tanstack/react-query";
import functions from '@react-native-firebase/functions';
import { Alert } from 'react-native';
import { useSignOut } from './useSignOut';
export const useDeleteFamily = () => {
const { user } = useAuthContext();
const signOut = useSignOut();
return useMutation({
mutationKey: ["deleteFamily"],
@ -16,6 +18,11 @@ export const useDeleteFamily = () => {
try {
const deleteFamilyFunction = functions().httpsCallable('deleteFamily');
const result = await deleteFamilyFunction({ familyId });
if (result.data.success) {
await signOut.mutateAsync();
}
return result.data;
} catch (error: any) {
if (error.code === 'permission-denied') {

View File

@ -1,30 +1,49 @@
import {useMutation} from "@tanstack/react-query";
import auth from "@react-native-firebase/auth";
import firestore from "@react-native-firebase/firestore";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {useSetUserData} from "./useSetUserData";
import {uuidv4} from "@firebase/util";
import * as Localization from "expo-localization";
export const useSignUp = () => {
const {setRedirectOverride} = useAuthContext()
const {setRedirectOverride} = useAuthContext();
const {mutateAsync: setUserData} = useSetUserData();
const createHouseholdIfNeeded = async (familyId: string, lastName: string) => {
try {
const householdRef = firestore().collection("Households");
const snapshot = await householdRef.where("familyId", "==", familyId).get();
if (snapshot.empty) {
await householdRef.add({
familyId,
name: lastName
});
}
} catch (error) {
console.error("Error creating household:", error);
throw error;
}
};
return useMutation({
mutationKey: ["signUp"],
mutationFn: async ({
email,
password,
firstName,
lastName,
birthday
}: {
email,
password,
firstName,
lastName,
birthday
}: {
email: string;
password: string;
firstName: string;
lastName: string;
birthday: Date;
}) => {
setRedirectOverride(true)
setRedirectOverride(true);
const familyId = uuidv4();
await auth()
.createUserWithEmailAndPassword(email, password)
@ -35,12 +54,14 @@ export const useSignUp = () => {
userType: ProfileType.PARENT,
firstName: firstName,
lastName: lastName,
familyId: uuidv4(),
familyId: familyId,
timeZone: Localization.getCalendars()[0].timeZone,
birthday: birthday
},
customUser: res.user,
});
await createHouseholdIfNeeded(familyId, lastName);
} catch (error) {
console.error(error);
}

View File

@ -59,14 +59,15 @@ export const useUpdateTodo = () => {
const newDate = nextDates[index];
let assignee;
if (todoData.assignees && todoData.rotate && todoData?.assignees?.length !== 0) {
assignee = todoData.assignees[index % todoData.assignees.length];
assignee = todoData.assignees[(index + 1) % todoData.assignees.length];
}
if (newDate) {
const nextTodo = {
...todoData,
date: newDate,
assignees: assignee ? [assignee] : todoData.assignees
assignees: todoData.assignees,
currentAssignee: assignee
}
let docRef = todo.ref;
batch.update(docRef, nextTodo)
@ -83,7 +84,7 @@ export const useUpdateTodo = () => {
let assignee;
if (todoData.assignees && todoData.rotate && todoData?.assignees?.length !== 0) {
assignee = todoData.assignees[index % todoData.assignees.length];
assignee = todoData.assignees[(index + 1) % todoData.assignees.length];
}
const nextTodo = {
@ -93,7 +94,8 @@ export const useUpdateTodo = () => {
familyId: profileData?.familyId,
creatorId: currentUser?.uid,
connectedTodoId: todoData.connectedTodoId,
assignees: assignee ? [assignee] : todoData.assignees
assignees: todoData.assignees,
currentAssignee: assignee
}
batch.set(newDocRef, nextTodo)