Merge branch 'dev'

This commit is contained in:
ivic00
2024-12-10 15:56:44 +01:00
13 changed files with 308 additions and 200 deletions

View File

@ -320,20 +320,14 @@ export default function TabLayout() {
name="index"
options={{
drawerLabel: "Calendar",
title:
Device.deviceType === DeviceType.TABLET
? "Family Calendar"
: "Calendar",
title: "Calendar",
}}
/>
<Drawer.Screen
name="calendar"
options={{
drawerLabel: "Calendar",
title:
Device.deviceType === DeviceType.TABLET
? "Family Calendar"
: "Calendar",
title: "Calendar",
drawerItemStyle: { display: "none" },
}}
/>

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import {RefreshControl, ScrollView, View} from "react-native";
import CalendarPage from "@/components/pages/calendar/CalendarPage";
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
@ -6,10 +6,20 @@ import * as Device from "expo-device";
import {DeviceType} from "expo-device";
import {useCalSync} from "@/hooks/useCalSync";
import {colorMap} from "@/constants/colorMap";
import { useSetAtom } from "jotai";
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
import { useAuthContext } from "@/contexts/AuthContext";
export default function Screen() {
const isTablet = Device.deviceType === DeviceType.TABLET;
const {resyncAllCalendars, isSyncing} = useCalSync();
const setSelectedUser = useSetAtom(selectedUserAtom);
const {profileData} = useAuthContext()
useEffect(() => {
if(!isTablet && profileData) setSelectedUser({firstName: profileData.firstName, lastName: profileData.lastName, eventColor: profileData.eventColor})
}, [])
const onRefresh = React.useCallback(async () => {
try {

View File

@ -39,7 +39,15 @@ const groupToDosByDate = (toDos: IToDo[]) => {
});
};
if (toDo.date === null || isToday(toDo.date)) {
const isOverdue = (date: Date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
};
if (isOverdue(toDo.date) && !toDo.done) {
dateKey = "Overdue";
} else if (toDo.date === null || isToday(toDo.date)) {
dateKey = "Today";
} else if (isTomorrow(toDo.date)) {
dateKey = "Tomorrow";
@ -49,7 +57,8 @@ const groupToDosByDate = (toDos: IToDo[]) => {
dateKey = "Next 30 Days";
subDateKey = format(toDo.date, "MMM d");
} else {
return groups;
dateKey = "Later";
subDateKey = format(toDo.date, "MMM d, yyyy");
}
if (!groups[dateKey]) {
@ -59,7 +68,7 @@ const groupToDosByDate = (toDos: IToDo[]) => {
};
}
if (dateKey === "Next 30 Days" && subDateKey) {
if ((dateKey === "Next 30 Days" || dateKey === "Later") && subDateKey) {
if (!groups[dateKey].subgroups[subDateKey]) {
groups[dateKey].subgroups[subDateKey] = [];
}
@ -103,14 +112,17 @@ const SingleUserChoreList = ({ user }: { user: UserProfile }) => {
};
const noDateToDos = groupedToDos["No Date"]?.items || [];
const datedToDos = Object.keys(groupedToDos).filter(
(key) => key !== "No Date"
);
const datedToDos = Object.keys(groupedToDos)
.filter((key) => key !== "No Date")
.sort((a, b) => {
const order = ["Overdue", "Today", "Tomorrow", "Next 7 Days", "Next 30 Days", "Later"];
return order.indexOf(a) - order.indexOf(b);
});
const renderTodoGroup = (dateKey: string) => {
const isExpanded = expandedGroups[dateKey] || false;
if (dateKey === "Next 30 Days") {
if (dateKey === "Next 30 Days" || dateKey === "Later") {
const subgroups = Object.entries(groupedToDos[dateKey].subgroups).sort(
([dateA], [dateB]) => {
const dateAObj = new Date(dateA);

View File

@ -5,14 +5,16 @@ import TabletContainer from "../tablet_components/TabletContainer";
import SingleUserChoreList from "./SingleUserChoreList";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import { ImageBackground, StyleSheet } from "react-native";
import { colorMap } from "@/constants/colorMap";
import { ScrollView } from "react-native-gesture-handler";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import AddChore from "../../todos/AddChore";
import { useAtom } from "jotai";
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
const TabletChoresPage = () => {
const {data: users} = useGetFamilyMembers();
const { user: currentUser } = useAuthContext();
const [selectedUser] = useAtom(selectedUserAtom);
const sortedUsers = useMemo(() => {
return users
@ -31,79 +33,77 @@ const TabletChoresPage = () => {
});
}, [users, currentUser]);
// Function to lock the screen orientation to landscape
const lockScreenOrientation = async () => {
await ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
);
};
useEffect(() => {
lockScreenOrientation(); // Lock orientation when the component mounts
useEffect(() => {
lockScreenOrientation();
return () => {
// Optional: Unlock to default when the component unmounts
ScreenOrientation.unlockAsync();
return () => {
ScreenOrientation.unlockAsync();
};
}, []);
const capitalizeFirstLetter = (str: string) => {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};
}, []);
const capitalizeFirstLetter = (str: string) => {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};
return (
<TabletContainer>
<ScrollView horizontal>
<View row gap-25 padding-25>
{sortedUsers
?.filter((member) => member.userType !== ProfileType.FAMILY_DEVICE)
.map((user, index) => (
<View key={index}>
<View row centerV>
{user.pfp ? (
<ImageBackground
source={{ uri: user.pfp }}
style={
styles.pfp
}
imageStyle={(user.eventColor && {
borderWidth: 2,
borderColor: user.eventColor,
}) ||
undefined
}
borderRadius={13.33}
/>
) : (
<View
center
style={styles.pfp}
backgroundColor={user.eventColor || "#00a8b6"}
>
<Text color="white">
{user.firstName.at(0)}
{user.lastName.at(0)}
</Text>
</View>
)}
<Text style={styles.name} marginL-15>
{user.firstName}
</Text>
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
({capitalizeFirstLetter(user.userType)})
</Text>
</View>
<SingleUserChoreList user={user} />
return (
<TabletContainer>
<ScrollView horizontal>
<View row gap-25 padding-25>
{sortedUsers
?.filter((member) =>
!selectedUser ||
selectedUser.uid === 'family-view' ||
selectedUser.uid === member.uid
)
.map((user, index) => (
<View key={index}>
<View row centerV>
{user.pfp ? (
<ImageBackground
source={{ uri: user.pfp }}
style={styles.pfp}
imageStyle={(user.eventColor && {
borderWidth: 2,
borderColor: user.eventColor,
}) || undefined}
borderRadius={13.33}
/>
) : (
<View
center
style={styles.pfp}
backgroundColor={user.eventColor || "#00a8b6"}
>
<Text color="white">
{user.firstName.at(0)}
{user.lastName.at(0)}
</Text>
</View>
)}
<Text style={styles.name} marginL-15>
{user.firstName}
</Text>
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
({capitalizeFirstLetter(user.userType)})
</Text>
</View>
<SingleUserChoreList user={user} />
</View>
))}
</View>
</ScrollView>
<View style={styles.addBtn}>
<AddChore />
</View>
))}
</View>
</ScrollView>
<View style={styles.addBtn}>
<AddChore />
</View>
</TabletContainer>
);
</TabletContainer>
);
};
const styles = StyleSheet.create({
@ -118,10 +118,10 @@ const styles = StyleSheet.create({
color: "#2c2c2c",
},
addBtn: {
position: 'absolute',
bottom: 50,
right: 220
position: 'absolute',
bottom: 50,
right: 220
}
});
export default TabletChoresPage;
export default TabletChoresPage;

View File

@ -1,37 +1,42 @@
import { View, Text } from "react-native-ui-lib";
import { View, Text, TouchableOpacity } from "react-native-ui-lib";
import React, { useEffect, useMemo } from "react";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import { ImageBackground, StyleSheet } from "react-native";
import { colorMap } from "@/constants/colorMap";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { ScrollView } from "react-native-gesture-handler";
import { useAtom } from "jotai";
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
const UsersList = () => {
const { user: currentUser } = useAuthContext();
const { data: familyMembers, refetch: refetchFamilyMembers } =
useGetFamilyMembers();
const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
useEffect(() => {
refetchFamilyMembers();
}, []);
const sortedMembers = useMemo(() => {
return familyMembers
?.filter((member) => member.userType !== ProfileType.FAMILY_DEVICE)
.sort((a, b) => {
// Current user first
if (a.uid === currentUser?.uid) return -1;
if (b.uid === currentUser?.uid) return 1;
// Then sort by user type priority
const typePriority = {
[ProfileType.PARENT]: 0,
[ProfileType.CHILD]: 1,
[ProfileType.CAREGIVER]: 2,
};
return typePriority[a.userType] - typePriority[b.userType];
});
const filtered = familyMembers
?.filter((member) => member.userType !== ProfileType.FAMILY_DEVICE);
const currentUserData = filtered?.find(m => m.uid === currentUser?.uid);
const parents = filtered?.filter(m => m.userType === ProfileType.PARENT && m.uid !== currentUser?.uid) || [];
const children = filtered?.filter(m => m.userType === ProfileType.CHILD && m.uid !== currentUser?.uid) || [];
const caregivers = filtered?.filter(m => m.userType === ProfileType.CAREGIVER && m.uid !== currentUser?.uid) || [];
const familyViewOption = {
uid: 'family-view',
firstName: 'Family',
lastName: 'View',
userType: 'Family View',
eventColor: colorMap.pink
};
return currentUserData
? [currentUserData, ...parents, ...children, familyViewOption, ...caregivers]
: [...parents, ...children, familyViewOption, ...caregivers];
}, [familyMembers, currentUser]);
const capitalizeFirstLetter = (str: string) => {
@ -39,37 +44,54 @@ const UsersList = () => {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};
useEffect(() => {
console.log(selectedUser);
}, [selectedUser]);
return (
<View centerH paddingT-10 marginB-70>
{sortedMembers?.map((member, index) => (
<React.Fragment key={member.uid}>
{member.pfp ? (
<ImageBackground
key={index}
source={{ uri: member.pfp }}
style={styles.pfp}
borderRadius={200}
imageStyle={{ borderWidth: 2, borderColor: "red" }}
/>
) : (
<View
key={index}
style={styles.pfp}
center
backgroundColor={member.eventColor || colorMap.teal}
>
<Text color="white">
{member.firstName.at(0)}
{member.lastName.at(0)}
</Text>
</View>
)}
<Text style={styles.fName}>{member.firstName}</Text>
<Text style={styles.role}>
{capitalizeFirstLetter(member.userType)}
</Text>
</React.Fragment>
))}
{sortedMembers?.map((member, index) => (
<TouchableOpacity
key={member.uid}
onPress={() => {
if (member.uid === 'family-view') {
setSelectedUser(null);
} else {
setSelectedUser(selectedUser?.uid === member.uid ? null : member);
}
}}
style={[
styles.memberContainer,
selectedUser?.uid === member.uid && styles.selectedMember,
]}
>
{member.pfp ? (
<ImageBackground
key={index}
source={{ uri: member.pfp }}
style={styles.pfp}
borderRadius={200}
imageStyle={{ borderWidth: 2, borderColor: "red" }}
/>
) : (
<View
key={index}
style={styles.pfp}
center
backgroundColor={member.eventColor || colorMap.teal}
>
<Text color="white">
{member.firstName.at(0)}
{member.lastName.at(0)}
</Text>
</View>
)}
<Text style={styles.fName}>{member.firstName}</Text>
<Text style={styles.role}>
{capitalizeFirstLetter(member.userType)}
</Text>
</TouchableOpacity>
))}
</View>
);
};
@ -91,6 +113,13 @@ const styles = StyleSheet.create({
color: "#9b9b9b",
marginBottom: 20,
},
memberContainer: {
alignItems: "center",
marginBottom: 20,
},
selectedMember: {
opacity: 1,
},
});
export default UsersList;

View File

@ -11,6 +11,7 @@ import {
modeAtom,
selectedDateAtom,
selectedNewEventDateAtom,
selectedUserAtom,
} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
@ -21,6 +22,8 @@ import {useSyncEvents} from "@/hooks/useSyncOnScroll";
import {colorMap} from "@/constants/colorMap";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import CachedImage from "expo-cached-image";
import { DeviceType } from "expo-device";
import * as Device from "expo-device"
interface EventCalendarProps {
calendarHeight: number;
@ -377,6 +380,9 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
const [mode, setMode] = useAtom(modeAtom);
const [isFamilyView] = useAtom(isFamilyViewAtom);
//tablet view filter
const [selectedUser] = useAtom(selectedUserAtom);
const setEditVisible = useSetAtom(editVisibleAtom);
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
const setEventForEdit = useSetAtom(eventForEditAtom);
@ -488,8 +494,17 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
const startOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
const endOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
let eventsToFilter = events;
if (selectedUser && Device.deviceType === DeviceType.TABLET) {
eventsToFilter = events?.filter(event =>
event.attendees?.includes(selectedUser.uid) ||
event.creatorId === selectedUser.uid
);
}
const filteredEvents =
events?.filter(
eventsToFilter?.filter(
(event) =>
event.start &&
event.end &&
@ -521,7 +536,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
return {enrichedEvents, filteredEvents};
}, [events, selectedDate, mode]);
}, [events, selectedDate, mode, selectedUser]);
const renderCustomDateForMonth = (date: Date) => {
const circleStyle = useMemo<ViewStyle>(

View File

@ -18,4 +18,10 @@ export const settingsPageIndex = atom<number>(0);
export const userSettingsView = atom<boolean>(true);
export const toDosPageIndex = atom<number>(0);
export const refreshTriggerAtom = atom<boolean>(false);
export const refreshEnabledAtom = atom<boolean>(true);
export const refreshEnabledAtom = atom<boolean>(true);
export const selectedUserAtom = atom<{
uid: string;
firstName: string;
lastName: string;
eventColor?: string;
} | null>(null);

View File

@ -113,24 +113,24 @@ const EditGroceryItem = ({
}}
maxLength={25}
/>
<View row centerV>
{(editGrocery.title || editGrocery.title !== "") && <View row centerV>
<AntDesign
name="check"
size={24}
style={{
color: "green",
marginRight: 15,
}}
onPress={handleSubmit}
name="check"
size={24}
style={{
color: "green",
marginRight: 15,
}}
onPress={handleSubmit}
/>
<CloseXIcon
onPress={() => {
if (editGrocery.closeEdit) {
editGrocery.closeEdit();
}
}}
onPress={() => {
if (editGrocery.closeEdit) {
editGrocery.closeEdit();
}
}}
/>
</View>
</View>}
</View>
<Dropdown
style={{ marginTop: 15 }}

View File

@ -134,17 +134,29 @@ const GroceryItem = ({
) : (
!isEditingTitle &&
(isParent || isCaregiver) && (
<Checkbox
value={item.bought}
containerStyle={[styles.checkbox, { borderRadius: 50 }]}
style={styles.checked}
borderRadius={50}
color="#fd1575"
hitSlop={20}
onValueChange={() =>
updateGroceryItem({ id: item.id, bought: !item.bought })
}
/>
<View row>
{item.bought &&
<AntDesign
name="close"
size={24}
style={{ color: "grey", marginRight: 10 }}
onPress={() => {
handleItemApproved(item.id, { approved: false });
deleteGrocery(item.id);
}}/>
}
<Checkbox
value={item.bought}
containerStyle={[styles.checkbox, { borderRadius: 50 }]}
style={styles.checked}
borderRadius={50}
color="#fd1575"
hitSlop={20}
onValueChange={() =>
updateGroceryItem({ id: item.id, bought: !item.bought })
}
/>
</View>
)
)}
</View>

View File

@ -8,7 +8,21 @@ import {AntDesign} from "@expo/vector-icons";
import EditGroceryItem from "./EditGroceryItem";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {IGrocery} from "@/hooks/firebase/types/groceryData";
import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
import Ionicons from '@expo/vector-icons/Ionicons';
import AddChoreDialog from "@/components/pages/todos/AddChoreDialog";
import {REPEAT_TYPE} from "@/hooks/firebase/types/todoData";
import {ToDosContextProvider} from "@/contexts/ToDosContext";
const shoppingTodo = {
id: "",
title: "Go shopping",
points: 10,
date: new Date(),
rotate: false,
repeatType: REPEAT_TYPE.NONE,
assignees: [],
repeatDays: []
};
const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
const {
@ -34,11 +48,15 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
const [pendingVisible, setPendingVisible] = useState<boolean>(true);
const [approvedVisible, setApprovedVisible] = useState<boolean>(true);
const [choreDialogVisible, setChoreDialogVisible] = useState<boolean>(false);
// Group approved groceries by category
const approvedGroceriesByCategory = approvedGroceries?.reduce(
let approvedGroceriesByCategory = approvedGroceries?.reduce(
(groups: any, item: IGrocery) => {
const category = item.category || "Uncategorized";
let category = item.category || "Uncategorized";
if (item.bought) {
category = "Done";
}
if (!groups[category]) {
groups[category] = [];
}
@ -80,6 +98,12 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
setPendingGroceries(groceries?.filter((item) => !item.approved));
}, [groceries]);
const handleCancelAddGrocery = () => {
setIsAddingGrocery(false);
setTitle("");
setCategory(GroceryCategory.None)
}
return (
<View marginH-20 marginB-45>
<HeaderTemplate
@ -89,10 +113,8 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
>
<View row centerV>
<View
backgroundColor="#e2eed8"
paddingH-15
paddingV-8
marginR-5
centerV
style={{borderRadius: 50}}
>
@ -110,7 +132,6 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
</Text>
</View>
<View
backgroundColor="#faead2"
padding-8
paddingH-12
marginR-15
@ -120,9 +141,15 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
{pendingGroceries?.length} pending
</Text>
</View>
<TouchableOpacity>
<AddPersonIcon width={24}/>
<TouchableOpacity onPress={() => setChoreDialogVisible(true)}>
<Ionicons name="person-add-outline" size={24} color="grey" />
</TouchableOpacity>
<ToDosContextProvider>
{choreDialogVisible && <AddChoreDialog
isVisible={choreDialogVisible}
setIsVisible={setChoreDialogVisible}
selectedTodo={shoppingTodo} /> }
</ToDosContextProvider>
</View>
</HeaderTemplate>
@ -220,27 +247,29 @@ const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
</Text>
</View>
</View>
{isAddingGrocery && (
<View style={{marginTop: 8}}>
<EditGroceryItem
editGrocery={{
title: title,
setCategory: setCategory,
category: category,
setTitle: setTitle,
setSubmit: setSubmitted,
closeEdit: () => setIsAddingGrocery(false)
}}
onInputFocus={onInputFocus}
/>
</View>
)}
<View style={{marginTop: 8}}>
<EditGroceryItem
editGrocery={{
title: title,
setCategory: setCategory,
category: category,
setTitle: setTitle,
setSubmit: setSubmitted,
closeEdit: handleCancelAddGrocery
}}
onInputFocus={onInputFocus}
/>
</View>
{/* Render Approved Groceries Grouped by Category */}
{approvedGroceries?.length > 0
? approvedVisible && (
<FlatList
data={Object.keys(approvedGroceriesByCategory)}
data={Object.keys(approvedGroceriesByCategory).sort((a, b) => {
if (a !== "Done") return -1;
if (b === "Done") return 1;
return 0;
})}
renderItem={({item: category}) => (
<View key={category}>
{/* Render Category Header */}

View File

@ -71,7 +71,6 @@ const GroceryWrapper = () => {
<GroceryList onInputFocus={handleInputFocus} />
</View>
</ScrollView>
{!isAddingGrocery && <AddGroceryItem />}
</>
);
};

View File

@ -240,8 +240,8 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
if (value) {
setTodo((oldValue) => ({
...oldValue,
date: new Date(),
repeatType: value.toString(),
repeatDays: []
}));
}
}}
@ -315,20 +315,22 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
setSelectedAttendees={setSelectedAssignees}
/>
</View>
<View row centerV style={styles.rotateSwitch}>
<Text style={{fontFamily: "PlusJakartaSans_500Medium", fontSize: 16}}>
Take Turns
</Text>
<Switch
onColor={"#ea156c"}
value={todo.rotate}
style={{width: 43.06, height: 27.13}}
marginL-10
onValueChange={(value) =>
setTodo((oldValue) => ({...oldValue, rotate: value}))
}
/>
</View>
{todo.repeatType !== REPEAT_TYPE.NONE && selectedAssignees?.length > 1 &&
<View row centerV style={styles.rotateSwitch}>
<Text style={{fontFamily: "PlusJakartaSans_500Medium", fontSize: 16}}>
Take Turns
</Text>
<Switch
onColor={"#ea156c"}
value={todo.rotate}
style={{width: 43.06, height: 27.13}}
marginL-10
onValueChange={(value) =>
setTodo((oldValue) => ({...oldValue, rotate: value}))
}
/>
</View>
}
<View style={styles.divider}/>
<View marginH-30 marginB-15 row centerV>
<Ionicons name="gift-outline" size={25} color="#919191"/>

View File

@ -42,8 +42,8 @@ export const useUpdateSubUser = () => {
}
},
onSuccess: () => {
queryClient.invalidateQueries({queryKey: ["getChildrenByParentId"]})
queryClient.invalidateQueries({queryKey: ["familyMembers"]})
queryClient.invalidateQueries({queryKey: ["profiles"]})
}
});
}