Merge branch 'dev'

# Conflicts:
#	ios/cally/Info.plist
This commit is contained in:
Milan Paunovic
2024-10-28 01:27:48 +01:00
13 changed files with 1652 additions and 1454 deletions

View File

@ -1,123 +1,119 @@
import { Dimensions, ScrollView } from "react-native"; import {Dimensions, ScrollView, StyleSheet} from "react-native";
import React, { useState } from "react"; import React, {useState} from "react";
import { View, Text, Button } from "react-native-ui-lib"; import {Button, Text, TextField, View} from "react-native-ui-lib";
import DumpList from "./DumpList"; import DumpList from "./DumpList";
import HeaderTemplate from "@/components/shared/HeaderTemplate"; import HeaderTemplate from "@/components/shared/HeaderTemplate";
import { TextField } from "react-native-ui-lib"; import {Feather, MaterialIcons} from "@expo/vector-icons";
import { StyleSheet } from "react-native";
import { Feather, MaterialIcons } from "@expo/vector-icons";
import { TextInput } from "react-native-gesture-handler";
import AddBrainDump from "./AddBrainDump"; import AddBrainDump from "./AddBrainDump";
import LinearGradient from "react-native-linear-gradient"; import LinearGradient from "react-native-linear-gradient";
const BrainDumpPage = () => { const BrainDumpPage = () => {
const [searchText, setSearchText] = useState<string>(""); const [searchText, setSearchText] = useState<string>("");
const [isAddVisible, setIsAddVisible] = useState<boolean>(false); const [isAddVisible, setIsAddVisible] = useState<boolean>(false);
return ( return (
<View height={"100%"}> <View height={"100%"}>
<View>
<ScrollView
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
>
<View marginH-25>
<HeaderTemplate
message={"Welcome to your notes!"}
isWelcome={false}
children={
<Text
style={{ fontFamily: "Manrope_400Regular", fontSize: 14 }}
>
Drop your notes on-the-go here, and{"\n"}organize them later.
</Text>
}
/>
<View> <View>
<View style={styles.searchField} centerV> <ScrollView
<TextField showsVerticalScrollIndicator={false}
value={searchText} showsHorizontalScrollIndicator={false}
onChangeText={(value) => { >
setSearchText(value); <View marginH-25>
}} <HeaderTemplate
leadingAccessory={ message={"Welcome to your notes!"}
<Feather isWelcome={false}
name="search" children={
size={24} <Text
color="#9b9b9b" style={{fontFamily: "Manrope_400Regular", fontSize: 14}}
style={{ paddingRight: 10 }} >
/> Drop your notes on-the-go here, and{"\n"}organize them later.
} </Text>
style={{ }
fontFamily: "Manrope_500Medium", />
fontSize: 15, <View>
}} <View style={styles.searchField} centerV>
placeholder="Search notes..." <TextField
/> value={searchText}
</View> onChangeText={(value) => {
<DumpList searchText={searchText} /> setSearchText(value);
}}
leadingAccessory={
<Feather
name="search"
size={24}
color="#9b9b9b"
style={{paddingRight: 10}}
/>
}
style={{
fontFamily: "Manrope_500Medium",
fontSize: 15,
}}
placeholder="Search notes..."
/>
</View>
<DumpList searchText={searchText}/>
</View>
</View>
</ScrollView>
</View> </View>
</View> <LinearGradient
</ScrollView> colors={["#f9f8f700", "#f9f8f7"]}
</View> locations={[0,1]}
<LinearGradient style={{
colors={["#f2f2f2", "transparent"]} position: "absolute",
start={{ x: 0.5, y: 1 }} bottom: 0,
end={{ x: 0.5, y: 0 }} height: 120,
style={{ width: Dimensions.get("screen").width,
position: "absolute", justifyContent:'center',
bottom: 0, alignItems:"center"
height: 90, }}
width: Dimensions.get("screen").width,
}}
>
<Button
style={{
height: 40,
position: "relative",
marginLeft: "auto",
width: 20,
right: 20,
bottom: -10,
borderRadius: 30,
backgroundColor: "#fd1775",
}}
color="white"
enableShadow
onPress={() => {
setIsAddVisible(true);
}}
>
<View row centerV centerH>
<MaterialIcons name="add" size={22} color={"white"} />
<Text
white
style={{ fontSize: 16, fontFamily: "Manrope_600SemiBold" }}
> >
New <Button
</Text> style={{
</View> height: 40,
</Button> position: "relative",
</LinearGradient> width: "90%",
<AddBrainDump bottom: -10,
addBrainDumpProps={{ borderRadius: 30,
isVisible: isAddVisible, backgroundColor: "#fd1775",
setIsVisible: setIsAddVisible, }}
}} color="white"
/> enableShadow
</View> onPress={() => {
); setIsAddVisible(true);
}}
>
<View row centerV centerH>
<MaterialIcons name="add" size={22} color={"white"}/>
<Text
white
style={{fontSize: 16, fontFamily: "Manrope_600SemiBold"}}
>
New
</Text>
</View>
</Button>
</LinearGradient>
<AddBrainDump
addBrainDumpProps={{
isVisible: isAddVisible,
setIsVisible: setIsAddVisible,
}}
/>
</View>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
searchField: { searchField: {
borderWidth: 0.7, borderWidth: 0.7,
borderColor: "#9b9b9b", borderColor: "#9b9b9b",
borderRadius: 15, borderRadius: 15,
height: 42, height: 42,
paddingLeft: 10, paddingLeft: 10,
marginVertical: 20, marginVertical: 20,
}, },
}); });
export default BrainDumpPage; export default BrainDumpPage;

View File

@ -1,134 +1,125 @@
import React, { memo } from "react"; import React, {memo} from "react";
import { import {Button, Picker, PickerModes, SegmentedControl, Text, View,} from "react-native-ui-lib";
Button, import {MaterialIcons} from "@expo/vector-icons";
Picker, import {modeMap, months} from "./constants";
PickerModes, import {StyleSheet} from "react-native";
SegmentedControl, import {useAtom} from "jotai";
Text, import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
View, import {isSameDay} from "date-fns";
} from "react-native-ui-lib"; import {useAuthContext} from "@/contexts/AuthContext";
import { MaterialIcons } from "@expo/vector-icons";
import { modeMap, months } from "./constants";
import { StyleSheet } from "react-native";
import { useAtom } from "jotai";
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
import { isSameDay } from "date-fns";
import { useAuthContext } from "@/contexts/AuthContext";
export const CalendarHeader = memo(() => { export const CalendarHeader = memo(() => {
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom); const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom); const [mode, setMode] = useAtom(modeAtom);
const { profileData } = useAuthContext(); const {profileData} = useAuthContext();
const handleSegmentChange = (index: number) => { const handleSegmentChange = (index: number) => {
const selectedMode = modeMap.get(index); const selectedMode = modeMap.get(index);
if (selectedMode) { if (selectedMode) {
setTimeout(() => { setTimeout(() => {
setMode(selectedMode as "day" | "week" | "month"); setMode(selectedMode as "day" | "week" | "month");
}, 150); }, 150);
} }
}; };
const handleMonthChange = (month: string) => { const handleMonthChange = (month: string) => {
const currentDay = selectedDate.getDate(); const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear(); const currentYear = selectedDate.getFullYear();
const newMonthIndex = months.indexOf(month); const newMonthIndex = months.indexOf(month);
const updatedDate = new Date(currentYear, newMonthIndex, currentDay); const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
setSelectedDate(updatedDate); setSelectedDate(updatedDate);
}; };
const isSelectedDateToday = isSameDay(selectedDate, new Date()); const isSelectedDateToday = isSameDay(selectedDate, new Date());
return ( return (
<View <View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "white",
marginBottom: 10,
}}
>
<View row centerV gap-3>
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}>
{selectedDate.getFullYear()}
</Text>
<Picker
value={months[selectedDate.getMonth()]}
placeholder={"Select Month"}
style={{ fontFamily: "Manrope_500Medium", fontSize: 17, width: 85 }}
mode={PickerModes.SINGLE}
onChange={(itemValue) => handleMonthChange(itemValue as string)}
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"} />}
topBarProps={{
title: selectedDate.getFullYear().toString(),
titleStyle: { fontFamily: "Manrope_500Medium", fontSize: 17 },
}}
>
{months.map((month) => (
<Picker.Item key={month} label={month} value={month} />
))}
</Picker>
</View>
<View row centerV>
{!isSelectedDateToday && (
<Button
size={"xSmall"}
marginR-0
avoidInnerPadding
padding-7
style={{ style={{
borderRadius: 5, flexDirection: "row",
backgroundColor: "white", justifyContent: "space-between",
borderWidth: 0.7, alignItems: "center",
borderColor: "#dadce0", paddingHorizontal: 10,
height: 30, paddingVertical: 8,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "white",
marginBottom: 10,
}} }}
labelStyle={{ >
fontSize: 12, <View row centerV gap-3>
color: "black", <Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
fontFamily: "Manrope_500Medium", {selectedDate.getFullYear()}
}} </Text>
label={new Date().toLocaleDateString("en-US", { <Picker
timeZone: profileData?.timeZone || "", value={months[selectedDate.getMonth()]}
})} placeholder={"Select Month"}
onPress={() => { style={{fontFamily: "Manrope_500Medium", fontSize: 17, width: 85}}
setSelectedDate(new Date()); mode={PickerModes.SINGLE}
setMode("day"); onChange={(itemValue) => handleMonthChange(itemValue as string)}
console.log(profileData?.timeZone) trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"}/>}
}} topBarProps={{
/> title: selectedDate.getFullYear().toString(),
)} titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17},
}}
>
{months.map((month) => (
<Picker.Item key={month} label={month} value={month}/>
))}
</Picker>
</View>
<View> <View row centerV>
<SegmentedControl {!isSelectedDateToday && (
segments={[{ label: "D" }, { label: "W" }, { label: "M" }]} <Button
backgroundColor="#ececec" size={"xSmall"}
inactiveColor="#919191" marginR-0
activeBackgroundColor="#ea156c" avoidInnerPadding
activeColor="white" style={{
outlineColor="white" borderRadius: 50,
outlineWidth={3} backgroundColor: "white",
segmentLabelStyle={styles.segmentslblStyle} borderWidth: 0.7,
onChangeIndex={handleSegmentChange} borderColor: "#dadce0",
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2} height: 30,
/> paddingHorizontal: 10
}}
labelStyle={{
fontSize: 12,
color: "black",
fontFamily: "Manrope_500Medium",
}}
label={new Date().toLocaleDateString("en-US", {
timeZone: profileData?.timeZone || "",
})}
onPress={() => {
setSelectedDate(new Date());
}}
/>
)}
<View>
<SegmentedControl
segments={[{label: "D"}, {label: "W"}, {label: "M"}]}
backgroundColor="#ececec"
inactiveColor="#919191"
activeBackgroundColor="#ea156c"
activeColor="white"
outlineColor="white"
outlineWidth={3}
segmentLabelStyle={styles.segmentslblStyle}
onChangeIndex={handleSegmentChange}
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
/>
</View>
</View>
</View> </View>
</View> );
</View>
);
}); });
const styles = StyleSheet.create({ const styles = StyleSheet.create({
segmentslblStyle: { segmentslblStyle: {
fontSize: 12, fontSize: 12,
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",
}, },
}); });

View File

@ -1,163 +1,255 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, {useCallback, useEffect, useMemo, useState} from "react";
import { Calendar } from "react-native-big-calendar"; import {Calendar} from "react-native-big-calendar";
import { ActivityIndicator, StyleSheet, View } from "react-native"; import {ActivityIndicator, StyleSheet, View, ViewStyle} from "react-native";
import { useGetEvents } from "@/hooks/firebase/useGetEvents"; import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import { useAtom, useSetAtom } from "jotai"; import {useAtom, useSetAtom} from "jotai";
import { import {
editVisibleAtom, editVisibleAtom,
eventForEditAtom, eventForEditAtom,
modeAtom, modeAtom,
selectedDateAtom, selectedDateAtom,
selectedNewEventDateAtom, selectedNewEventDateAtom,
} from "@/components/pages/calendar/atoms"; } from "@/components/pages/calendar/atoms";
import { useAuthContext } from "@/contexts/AuthContext"; import {useAuthContext} from "@/contexts/AuthContext";
import { CalendarEvent } from "@/components/pages/calendar/interfaces"; import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib";
interface EventCalendarProps { interface EventCalendarProps {
calendarHeight: number; calendarHeight: number;
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL // WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
calendarWidth: number; calendarWidth: number;
} }
const getTotalMinutes = () => { const getTotalMinutes = () => {
const date = new Date(); const date = new Date();
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200); return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
}; };
export const EventCalendar: React.FC<EventCalendarProps> = React.memo( export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
({ calendarHeight }) => { ({calendarHeight}) => {
const { data: events, isLoading } = useGetEvents(); const {data: events, isLoading} = useGetEvents();
const { profileData } = useAuthContext(); const {profileData} = useAuthContext();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom); const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom); const [mode, setMode] = useAtom(modeAtom);
const setEditVisible = useSetAtom(editVisibleAtom); const setEditVisible = useSetAtom(editVisibleAtom);
const setEventForEdit = useSetAtom(eventForEditAtom); const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom); const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const [isRendering, setIsRendering] = useState(true); const [isRendering, setIsRendering] = useState(true);
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes()); const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
useEffect(() => { const todaysDate = new Date()
if (events && mode) {
setIsRendering(true);
const timeout = setTimeout(() => {
setIsRendering(false);
}, 300);
return () => clearTimeout(timeout);
}
}, [events, mode]);
const handlePressEvent = useCallback( useEffect(() => {
(event: CalendarEvent) => { if (events && mode) {
if (mode === "day" || mode === "week") { setIsRendering(true);
setEditVisible(true); const timeout = setTimeout(() => {
console.log({ event }); setIsRendering(false);
setEventForEdit(event); }, 300);
} else { return () => clearTimeout(timeout);
setMode("day"); }
setSelectedDate(event.start); }, [events, mode]);
const handlePressEvent = useCallback(
(event: CalendarEvent) => {
if (mode === "day" || mode === "week") {
setEditVisible(true);
console.log({event});
setEventForEdit(event);
} else {
setMode("day");
setSelectedDate(event.start);
}
},
[setEditVisible, setEventForEdit, mode]
);
const handlePressCell = useCallback(
(date: Date) => {
if (mode === "day" || mode === "week") {
setSelectedNewEndDate(date);
} else {
setMode("day");
setSelectedDate(date);
}
},
[mode, setSelectedNewEndDate, setSelectedDate]
);
const handleSwipeEnd = useCallback(
(date: Date) => {
setSelectedDate(date);
},
[setSelectedDate]
);
const memoizedEventCellStyle = useCallback(
(event: CalendarEvent) => ({backgroundColor: event.eventColor}),
[]
);
const memoizedWeekStartsOn = useMemo(
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
[profileData]
);
const isSameDate = useCallback((date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
);
}, []);
const dayHeaderColor = useMemo(() => {
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
}, [selectedDate, mode]);
const dateStyle = useMemo(() => {
if (mode === "week") return undefined
return isSameDate(todaysDate, selectedDate) && mode === "day"
? styles.dayHeader
: styles.otherDayHeader;
}, [selectedDate, mode]);
const memoizedHeaderContentStyle = useMemo(() => {
if (mode === "day") {
return styles.dayModeHeader;
} else if (mode === "week") {
return styles.weekModeHeader;
} else if (mode === "month") {
return styles.monthModeHeader;
} else {
return {};
}
}, [mode]);
const memoizedEvents = useMemo(() => events ?? [], [events]);
const renderCustomDateForMonth = (date: Date) => {
const circleStyle = useMemo<ViewStyle>(
() => ({
position: "absolute",
width: 30,
height: 30,
justifyContent: "center",
alignItems: "center",
borderRadius: 15,
}),
[]
);
const defaultStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
}),
[circleStyle]
);
const currentDateStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
backgroundColor: "#4184f2",
}),
[circleStyle]
);
const renderDate = useCallback(
(date: Date) => {
const isCurrentDate = isSameDate(todaysDate, date);
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
return (
<View style={{alignItems: "center"}}>
<View style={appliedStyle}>
<Text style={{color: isCurrentDate ? "white" : "black"}}>
{date.getDate()}
</Text>
</View>
</View>
);
},
[todaysDate, currentDateStyle, defaultStyle] // dependencies
);
return renderDate(date);
};
useEffect(() => {
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading || isRendering) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff"/>
</View>
);
} }
},
[setEditVisible, setEventForEdit, mode]
);
const handlePressCell = useCallback( return (
(date: Date) => { <Calendar
if (mode === "day" || mode === "week") { bodyContainerStyle={styles.calHeader}
setSelectedNewEndDate(date); swipeEnabled
} else { enableEnrichedEvents
setMode("day"); mode={mode}
setSelectedDate(date); events={memoizedEvents}
} eventCellStyle={memoizedEventCellStyle}
}, onPressEvent={handlePressEvent}
[mode, setSelectedNewEndDate, setSelectedDate] weekStartsOn={memoizedWeekStartsOn}
); height={calendarHeight}
activeDate={todaysDate}
const handleSwipeEnd = useCallback( date={selectedDate}
(date: Date) => { onPressCell={handlePressCell}
setSelectedDate(date); headerContentStyle={memoizedHeaderContentStyle}
}, onSwipeEnd={handleSwipeEnd}
[setSelectedDate] scrollOffsetMinutes={offsetMinutes}
); dayHeaderStyle={dateStyle}
dayHeaderHighlightColor={dayHeaderColor}
const memoizedEventCellStyle = useCallback( renderCustomDateForMonth={renderCustomDateForMonth}
(event: CalendarEvent) => ({ backgroundColor: event.eventColor }), showAdjacentMonths
[] />
); );
const memoizedWeekStartsOn = useMemo(
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
[profileData]
);
const memoizedHeaderContentStyle = useMemo(
() => (mode === "day" ? styles.dayModeHeader : {}),
[mode]
);
const memoizedEvents = useMemo(() => events ?? [], [events]);
useEffect(() => {
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading || isRendering) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
);
} }
return (
<Calendar
bodyContainerStyle={styles.calHeader}
swipeEnabled
enableEnrichedEvents
mode={mode}
events={memoizedEvents}
eventCellStyle={memoizedEventCellStyle}
onPressEvent={handlePressEvent}
weekStartsOn={memoizedWeekStartsOn}
height={calendarHeight}
activeDate={selectedDate}
date={selectedDate}
onPressCell={handlePressCell}
headerContentStyle={memoizedHeaderContentStyle}
onSwipeEnd={handleSwipeEnd}
scrollOffsetMinutes={offsetMinutes}
/>
);
}
); );
const styles = StyleSheet.create({ const styles = StyleSheet.create({
segmentslblStyle: { segmentslblStyle: {
fontSize: 12, fontSize: 12,
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",
}, },
calHeader: { calHeader: {
borderWidth: 0, borderWidth: 0,
}, },
dayModeHeader: { dayModeHeader: {
alignSelf: "flex-start", alignSelf: "flex-start",
justifyContent: "space-between", justifyContent: "space-between",
alignContent: "center", alignContent: "center",
width: 38, width: 38,
right: 42, right: 42,
height: 13, height: 13,
}, },
loadingContainer: { weekModeHeader: {},
flex: 1, monthModeHeader: {},
justifyContent: "center", loadingContainer: {
alignItems: "center", flex: 1,
}, justifyContent: "center",
dayHeader: { alignItems: "center",
backgroundColor: "#4184f2", },
aspectRatio: 1, dayHeader: {
borderRadius: 100, backgroundColor: "#4184f2",
alignItems: "center", aspectRatio: 1,
justifyContent: "center", borderRadius: 100,
}, alignItems: "center",
justifyContent: "center",
},
otherDayHeader: {
backgroundColor: "transparent",
color: "#919191",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
}); });

View File

@ -1,96 +1,78 @@
import { StyleSheet } from "react-native"; import {StyleSheet} from "react-native";
import React, { useState } from "react"; import React from "react";
import { import {Button, View,} from "react-native-ui-lib";
Button, import {useGroceryContext} from "@/contexts/GroceryContext";
Colors, import {FontAwesome6} from "@expo/vector-icons";
Dialog,
Drawer,
Text,
View,
PanningProvider,
} from "react-native-ui-lib";
import { useGroceryContext } from "@/contexts/GroceryContext";
import { FontAwesome6 } from "@expo/vector-icons";
interface AddGroceryItemProps {
visible: boolean;
onClose: () => void;
}
const AddGroceryItem = () => {
const { isAddingGrocery, setIsAddingGrocery } = useGroceryContext();
const [visible, setVisible] = useState<boolean>(false);
const handleShowDialog = () => { const AddGroceryItem = () => {
setVisible(true); const {setIsAddingGrocery} = useGroceryContext();
};
const handleHideDialog = () => { return (
setVisible(false); <View
}; row
return ( spread
<View paddingH-25
row style={{
spread position: "absolute",
paddingH-25 bottom: 20,
style={{ width: "100%",
position: "absolute", height: 60,
bottom: 20, }}
width: "100%", >
height: 60, <View style={styles.btnContainer} row>
}} <Button
> color="white"
<View style={styles.btnContainer} row> backgroundColor="#fd1775"
<Button label="Add item"
color="white" text70L
backgroundColor="#fd1775" iconSource={() => <FontAwesome6 name="add" size={18} color="white"/>}
label="Add item" style={styles.finishShopBtn}
text70L labelStyle={styles.addBtnLbl}
iconSource={() => <FontAwesome6 name="add" size={18} color="white" />} enableShadow
style={styles.finishShopBtn} onPress={() => {
labelStyle={styles.addBtnLbl} setIsAddingGrocery(true);
enableShadow }}
onPress={() => { />
setIsAddingGrocery(true); </View>
}} </View>
/> );
</View>
</View>
);
}; };
export default AddGroceryItem; export default AddGroceryItem;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingVertical: 10, paddingVertical: 10,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
}, },
inner: { inner: {
paddingHorizontal: 20, paddingHorizontal: 20,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}, },
title: { title: {
fontSize: 20, fontSize: 20,
fontWeight: "400", fontWeight: "400",
textAlign: "center", textAlign: "center",
}, },
divider: { divider: {
width: "100%", width: "100%",
height: 1, height: 1,
backgroundColor: "#E0E0E0", backgroundColor: "#E0E0E0",
marginVertical: 10, marginVertical: 10,
}, },
btnContainer: { btnContainer: {
width: "100%", width: "100%",
justifyContent: "center", justifyContent: "center",
}, },
finishShopBtn: { finishShopBtn: {
width: "100%", width: "100%",
}, },
shoppingBtn: { shoppingBtn: {
flex: 1, flex: 1,
marginHorizontal: 3, marginHorizontal: 3,
}, },
addBtnLbl: { fontFamily: "Manrope_500Medium", fontSize: 17, marginLeft: 5 }, addBtnLbl: {fontFamily: "Manrope_500Medium", fontSize: 17, marginLeft: 5},
}); });

View File

@ -1,145 +1,167 @@
import { Text, View } from "react-native-ui-lib"; import {Text, TextField, TextFieldRef, View} from "react-native-ui-lib";
import React, { useEffect, useRef, useState } from "react"; import React, {useEffect, useRef} from "react";
import { TextField, TextFieldRef } from "react-native-ui-lib"; import {GroceryCategory, useGroceryContext} from "@/contexts/GroceryContext";
import { GroceryCategory, useGroceryContext } from "@/contexts/GroceryContext"; import {Dropdown} from "react-native-element-dropdown";
import { Dropdown } from "react-native-element-dropdown";
import CloseXIcon from "@/assets/svgs/CloseXIcon"; import CloseXIcon from "@/assets/svgs/CloseXIcon";
import { StyleSheet } from "react-native"; import {StyleSheet} from "react-native";
import DropdownIcon from "@/assets/svgs/DropdownIcon"; import DropdownIcon from "@/assets/svgs/DropdownIcon";
import {AntDesign} from "@expo/vector-icons";
interface IEditGrocery { interface IEditGrocery {
id?: string; id?: string;
title: string; title: string;
category: GroceryCategory; category: GroceryCategory;
setTitle: (value: string) => void; setTitle: (value: string) => void;
setCategory?: (category: GroceryCategory) => void; setCategory?: (category: GroceryCategory) => void;
setSubmit?: (value: boolean) => void; setSubmit?: (value: boolean) => void;
closeEdit?: (value: boolean) => void; closeEdit?: () => void;
handleEditSubmit?: Function; handleEditSubmit?: Function;
} }
const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
const { fuzzyMatchGroceryCategory } = useGroceryContext();
const inputRef = useRef<TextFieldRef>(null);
const groceryCategoryOptions = Object.values(GroceryCategory).map( const EditGroceryItem = ({editGrocery}: { editGrocery: IEditGrocery }) => {
(category) => ({ const {fuzzyMatchGroceryCategory} = useGroceryContext();
label: category, const inputRef = useRef<TextFieldRef>(null);
value: category,
})
);
useEffect(() => { const groceryCategoryOptions = Object.values(GroceryCategory).map(
if (inputRef.current) { (category) => ({
inputRef.current.focus(); // Focus on the TextField label: category,
} value: category,
console.log(editGrocery.category); })
}, []); );
return ( const handleSubmit = () => {
<View inputRef?.current?.blur()
style={{ console.log("CALLLLLL")
backgroundColor: "white", if (editGrocery.setSubmit) {
width: "100%", editGrocery.setSubmit(true);
borderRadius: 25, }
paddingHorizontal: 13, if (editGrocery.handleEditSubmit) {
paddingVertical: 10, editGrocery.handleEditSubmit({
marginTop: 0,
}}
>
<View row spread centerV>
<TextField
text70T
style={{}}
ref={inputRef}
placeholder="Grocery"
value={editGrocery.title}
onChangeText={(value) => {
editGrocery.setTitle(value);
}}
onSubmitEditing={() => {
if (editGrocery.setSubmit) {
editGrocery.setSubmit(true);
}
if (editGrocery.handleEditSubmit) {
editGrocery.handleEditSubmit({
id: editGrocery.id, id: editGrocery.id,
title: editGrocery.title, title: editGrocery.title,
category: editGrocery.category, category: editGrocery.category,
});
}
if (editGrocery.closeEdit) {
editGrocery.closeEdit(false);
}
}}
maxLength={25}
/>
<CloseXIcon
onPress={() => {
if (editGrocery.closeEdit) editGrocery.closeEdit(false);
}}
/>
</View>
<Dropdown
style={{marginTop: 15}}
data={groceryCategoryOptions}
placeholder="Select grocery category"
placeholderStyle={{ color: "#a2a2a2", fontFamily: "Manrope_500Medium", fontSize: 13.2 }}
labelField="label"
valueField="value"
value={
editGrocery.category == GroceryCategory.None
? null
: editGrocery.category
}
iconColor="white"
activeColor={"#fd1775"}
containerStyle={styles.dropdownStyle}
itemTextStyle={styles.itemText}
itemContainerStyle={styles.itemStyle}
selectedTextStyle={styles.selectedText}
renderLeftIcon={() => (
<DropdownIcon style={{ marginRight: 8 }} color={editGrocery.category == GroceryCategory.None ? "#7b7b7b" : "#fd1775"} />
)}
renderItem={(item) => {
return (
<View height={36.02} centerV>
<Text style={styles.itemText}>{item.label}</Text>
</View>
);
}}
onChange={(item) => {
if (editGrocery.handleEditSubmit) {
editGrocery.handleEditSubmit({
id: editGrocery.id,
category: item.value,
}); });
console.log("kategorija vo diropdown: " + item.value); }
if (editGrocery.closeEdit) editGrocery.closeEdit(false); if (editGrocery.closeEdit) {
} else { editGrocery.closeEdit();
if (editGrocery.setCategory) { }
editGrocery.setCategory(item.value); }
}
} useEffect(() => {
}} if (inputRef.current) {
/> inputRef.current.focus();
</View> }
);
console.log(editGrocery.category);
}, []);
return (
<View
style={{
backgroundColor: "white",
width: "100%",
borderRadius: 25,
paddingHorizontal: 13,
paddingVertical: 10,
marginTop: 0,
}}
>
<View row spread centerV>
<TextField
text70T
ref={inputRef}
placeholder="Grocery"
value={editGrocery.title}
onSubmitEditing={handleSubmit}
numberOfLines={1}
returnKeyType="done"
onChangeText={(value) => {
editGrocery.setTitle(value);
let groceryCategory = fuzzyMatchGroceryCategory(value);
if (editGrocery.setCategory) {
editGrocery.setCategory(groceryCategory);
}
}}
maxLength={25}
/>
<View row centerV>
<AntDesign
name="check"
size={24}
style={{
color: "green",
marginRight: 15,
}}
onPress={handleSubmit}
/>
<CloseXIcon
onPress={() => {
if (editGrocery.closeEdit) {
editGrocery.closeEdit();
}
}}
/>
</View>
</View>
<Dropdown
style={{marginTop: 15}}
data={groceryCategoryOptions}
placeholder="Select grocery category"
placeholderStyle={{color: "#a2a2a2", fontFamily: "Manrope_500Medium", fontSize: 13.2}}
labelField="label"
valueField="value"
value={
editGrocery.category
}
iconColor="white"
activeColor={"#fd1775"}
containerStyle={styles.dropdownStyle}
itemTextStyle={styles.itemText}
itemContainerStyle={styles.itemStyle}
selectedTextStyle={styles.selectedText}
renderLeftIcon={() => (
<DropdownIcon style={{marginRight: 8}}
color={editGrocery.category == GroceryCategory.None ? "#7b7b7b" : "#fd1775"}/>
)}
renderItem={(item) => {
return (
<View height={36.02} centerV>
<Text style={styles.itemText}>{item.label}</Text>
</View>
);
}}
onChange={(item) => {
if (editGrocery.handleEditSubmit) {
editGrocery.handleEditSubmit({
id: editGrocery.id,
category: item.value,
});
if (editGrocery.closeEdit) editGrocery.closeEdit();
} else {
if (editGrocery.setCategory) {
editGrocery.setCategory(item.value);
}
}
}}
/>
</View>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
itemText: { itemText: {
fontFamily: "Manrope_400Regular", fontFamily: "Manrope_400Regular",
fontSize: 15.42, fontSize: 15.42,
paddingLeft: 15, paddingLeft: 15,
}, },
selectedText: { selectedText: {
fontFamily: "Manrope_500Medium", fontFamily: "Manrope_500Medium",
fontSize: 13.2, fontSize: 13.2,
color: "#fd1775", color: "#fd1775",
}, },
dropdownStyle: { borderRadius: 6.61, height: 115.34, width: 187 }, dropdownStyle: {borderRadius: 6.61, height: 115.34, width: 187},
itemStyle: { padding: 0, margin: 0 }, itemStyle: {padding: 0, margin: 0},
}); });
export default EditGroceryItem; export default EditGroceryItem;

View File

@ -23,12 +23,14 @@ 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>(item.title ?? "");
const [category, setCategory] = useState<GroceryCategory>( const [category, setCategory] = useState<GroceryCategory>(item.category ?? GroceryCategory.None);
GroceryCategory.None
);
const [itemCreator, setItemCreator] = useState<UserProfile>(null); const [itemCreator, setItemCreator] = useState<UserProfile>(null);
const closeEdit = () => {
setIsEditingTitle(false);
}
const handleTitleChange = (newTitle: string) => { const handleTitleChange = (newTitle: string) => {
updateGroceryItem({ id: item?.id, title: newTitle }); updateGroceryItem({ id: item?.id, title: newTitle });
}; };
@ -38,7 +40,6 @@ const GroceryItem = ({
}; };
useEffect(() => { useEffect(() => {
setNewTitle(item.title);
console.log(item); console.log(item);
getItemCreator(item?.creatorId); getItemCreator(item?.creatorId);
}, []); }, []);
@ -81,52 +82,51 @@ const GroceryItem = ({
setOpenFreqEdit(false); setOpenFreqEdit(false);
}} }}
/> />
{!isEditingTitle ? ( {isEditingTitle ?
<View> <EditGroceryItem
{ isParent ? <TouchableOpacity onPress={() => setIsEditingTitle(true)}> editGrocery={{
<Text text70T black style={styles.title}> id: item.id,
{item.title} title: newTitle,
</Text> category: category,
</TouchableOpacity> : setTitle: setNewTitle,
setCategory: setCategory,
closeEdit: closeEdit,
handleEditSubmit: updateGroceryItem,
}}
/> :
<View>
{isParent ?
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
<Text text70T black style={styles.title}>
{item.title}
</Text>
</TouchableOpacity> :
<Text text70T black style={styles.title}> <Text text70T black style={styles.title}>
{item.title} {item.title}
</Text> </Text>
} }
</View> </View>
) : ( }
<EditGroceryItem
editGrocery={{
id: item.id,
title: newTitle,
category: item.category,
setTitle: setNewTitle,
setCategory: setCategory,
closeEdit: setIsEditingTitle,
handleEditSubmit: updateGroceryItem,
}}
/>
)}
{!item.approved ? ( {!item.approved ? (
<View row centerV marginB-10> <View row centerV marginB-10>
{isParent && <><AntDesign {isParent &&
name="check" <>
size={24} <AntDesign
style={{ name="check"
color: "green", size={24}
marginRight: 15, style={{
}} color: "green",
onPress={() => { marginRight: 15,
isParent ? handleItemApproved(item.id, { approved: true }) : null }}
}} onPress={isParent ? () => handleItemApproved(item.id, { approved: true }) : null}
/> />
<AntDesign <AntDesign
name="close" name="close"
size={24} size={24}
style={{ color: "red" }} style={{ color: "red" }}
onPress={() => { onPress={isParent ? () => handleItemApproved(item.id, { approved: false }) : null}
isParent ? handleItemApproved(item.id, { approved: false }) : null />
}} </>}
/> </>}
</View> </View>
) : ( ) : (
!isEditingTitle && ( !isEditingTitle && (
@ -158,14 +158,15 @@ const GroceryItem = ({
borderRadius: 22, borderRadius: 22,
overflow: "hidden", overflow: "hidden",
}} }}
/> : <View /> :
<View
style={{ style={{
position: "relative", position: "relative",
width: 24.64, width: 24.64,
aspectRatio: 1, aspectRatio: 1,
marginRight: 4 marginRight: 4
}} }}
> >
<View <View
style={{ style={{
backgroundColor: "#ccc", backgroundColor: "#ccc",
@ -177,13 +178,13 @@ const GroceryItem = ({
}} }}
> >
<Text <Text
style={{ style={{
color: "#fff", color: "#fff",
fontSize: 12, fontSize: 12,
fontWeight: "bold", fontWeight: "bold",
}} }}
> >
{getInitials(itemCreator.firstName, itemCreator.lastName ?? "")} {itemCreator ? getInitials(itemCreator.firstName, itemCreator.lastName) : ""}
</Text> </Text>
</View> </View>
</View>} </View>}

View File

@ -1,299 +1,294 @@
import { FlatList, StyleSheet } from "react-native"; import {FlatList, StyleSheet} from "react-native";
import React, { useEffect, useState } from "react"; import React, {useEffect, useState} from "react";
import { Button, Text, TouchableOpacity, View } from "react-native-ui-lib"; import {Text, TouchableOpacity, View} from "react-native-ui-lib";
import GroceryItem from "./GroceryItem"; import GroceryItem from "./GroceryItem";
import { import {GroceryCategory, GroceryFrequency, useGroceryContext,} from "@/contexts/GroceryContext";
GroceryCategory,
GroceryFrequency,
useGroceryContext,
} from "@/contexts/GroceryContext";
import HeaderTemplate from "@/components/shared/HeaderTemplate"; import HeaderTemplate from "@/components/shared/HeaderTemplate";
import { AntDesign, MaterialIcons } from "@expo/vector-icons"; import {AntDesign} 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"; import {IGrocery} from "@/hooks/firebase/types/groceryData";
import AddPersonIcon from "@/assets/svgs/AddPersonIcon"; import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
const GroceryList = () => { const GroceryList = () => {
const { const {
groceries, groceries,
updateGroceryItem, updateGroceryItem,
isAddingGrocery, isAddingGrocery,
setIsAddingGrocery, setIsAddingGrocery,
addGrocery, addGrocery,
} = 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)
); );
const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>( const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>(
groceries?.filter((item) => item.approved !== true) groceries?.filter((item) => !item.approved)
); );
const [category, setCategory] = useState<GroceryCategory>( const [category, setCategory] = useState<GroceryCategory>(
GroceryCategory.None GroceryCategory.None
); );
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [submit, setSubmitted] = useState<boolean>(false); const [submit, setSubmitted] = useState<boolean>(false);
const [pendingVisible, setPendingVisible] = useState<boolean>(true); const [pendingVisible, setPendingVisible] = useState<boolean>(true);
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]) {
groups[category] = []; groups[category] = [];
} }
groups[category].push(item); groups[category].push(item);
return groups; return groups;
}, },
{} {}
); );
useEffect(() => { useEffect(() => {
if (submit) { if (submit) {
if (title?.length > 2 && title?.length <= 25) { if (title?.length > 2 && title?.length <= 25) {
addGrocery({ addGrocery({
id: "", id: "",
title: title, title: title,
category: category, category: category,
approved: profileData?.userType === ProfileType.PARENT, approved: profileData?.userType === ProfileType.PARENT,
recurring: false, recurring: false,
frequency: GroceryFrequency.Never, frequency: GroceryFrequency.Never,
bought: false, bought: false,
}); });
setIsAddingGrocery(false); setIsAddingGrocery(false);
setSubmitted(false); setSubmitted(false);
setTitle(""); setTitle("");
} }
} }
}, [submit]); }, [submit]);
useEffect(() => { useEffect(() => {
/**/ setapprovedGroceries(groceries?.filter((item) => item.approved));
}, [category]); setPendingGroceries(groceries?.filter((item) => !item.approved));
}, [groceries]);
useEffect(() => { return (
setapprovedGroceries(groceries?.filter((item) => item.approved === true)); <View marginH-20 marginB-20>
setPendingGroceries(groceries?.filter((item) => item.approved !== true)); <HeaderTemplate
}, [groceries]); message={"Welcome to your grocery list"}
isWelcome={false}
return ( >
<View marginH-20 marginB-20> <View row centerV>
<HeaderTemplate <View
message={"Welcome to your grocery list"} backgroundColor="#e2eed8"
isWelcome={false} paddingH-15
> paddingV-8
<View row centerV> marginR-5
<View centerV
backgroundColor="#e2eed8" style={{borderRadius: 50}}
paddingH-15 >
paddingV-8 <Text text70BL color="#46a80a" style={styles.counterText}>
marginR-5 {approvedGroceries?.length} list{" "}
centerV {approvedGroceries?.length === 1 ? (
style={{ borderRadius: 50 }} <Text text70BL color="#46a80a" style={styles.counterText}>
> item
<Text text70BL color="#46a80a" style={styles.counterText}> </Text>
{approvedGroceries?.length} list{" "} ) : (
{approvedGroceries?.length === 1 ? ( <Text text70BL color="#46a80a" style={styles.counterText}>
<Text text70BL color="#46a80a" style={styles.counterText}> items
item </Text>
</Text> )}
) : ( </Text>
<Text text70BL color="#46a80a" style={styles.counterText}> </View>
items <View
</Text> backgroundColor="#faead2"
)} padding-8
</Text> paddingH-12
</View> marginR-15
<View style={{borderRadius: 50}}
backgroundColor="#faead2" >
padding-8 <Text text70 style={styles.counterText} color="#e28800">
paddingH-12 {pendingGroceries?.length} pending
marginR-15 </Text>
style={{ borderRadius: 50 }} </View>
> <TouchableOpacity>
<Text text70 style={styles.counterText} color="#e28800"> <AddPersonIcon width={24}/>
{pendingGroceries?.length} pending </TouchableOpacity>
</Text>
</View>
<TouchableOpacity>
<AddPersonIcon width={24}/>
</TouchableOpacity>
</View>
</HeaderTemplate>
{/* Pending Approval Section */}
<View row spread marginT-40 marginB-10 centerV>
<View row centerV>
<Text style={styles.subHeader}>Pending Approval</Text>
{pendingVisible && (
<AntDesign
name="down"
size={17}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setPendingVisible(false);
}}
/>
)}
{!pendingVisible && (
<AntDesign
name="right"
size={15}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setPendingVisible(true);
}}
/>
)}
</View>
<View
centerV
style={{
aspectRatio: 1,
width: 35,
backgroundColor: "#faead2",
borderRadius: 50,
}}
>
<Text style={styles.counterNr} center color="#e28800">
{pendingGroceries?.length.toString()}
</Text>
</View>
</View>
{pendingGroceries?.length > 0
? pendingVisible && (
<FlatList
data={pendingGroceries}
renderItem={({ item }) => (
<GroceryItem
item={item}
handleItemApproved={(id, changes) =>
updateGroceryItem({ ...changes, id: id })
}
/>
)}
keyExtractor={(item) => item.id.toString()}
/>
)
: pendingVisible && (
<Text style={styles.noItemTxt}>No items pending approval.</Text>
)}
{/* Approved Section */}
<View row spread marginT-40 marginB-0 centerV>
<View row centerV>
<Text style={styles.subHeader}>Shopping List</Text>
{approvedVisible && (
<AntDesign
name="down"
size={17}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setApprovedVisible(false);
}}
/>
)}
{!approvedVisible && (
<AntDesign
name="right"
size={15}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setApprovedVisible(true);
}}
/>
)}
</View>
<View
centerV
style={{
aspectRatio: 1,
width: 35,
backgroundColor: "#e2eed8",
borderRadius: 50,
}}
>
<Text style={styles.counterNr} center color="#46a80a">
{approvedGroceries?.length.toString()}
</Text>
</View>
</View>
{isAddingGrocery && (
<EditGroceryItem
editGrocery={{
title: title,
setCategory: setCategory,
category: category,
setTitle: setTitle,
setSubmit: setSubmitted,
}}
/>
)}
{/* Render Approved Groceries Grouped by Category */}
{approvedGroceries?.length > 0
? approvedVisible && (
<FlatList
data={Object.keys(approvedGroceriesByCategory)}
renderItem={({ item: 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 })
}
/>
)
)}
</View> </View>
)} </HeaderTemplate>
keyExtractor={(category) => category}
/> {/* Pending Approval Section */}
) <View row spread marginT-40 marginB-10 centerV>
: approvedVisible && ( <View row centerV>
<Text style={styles.noItemTxt}>No approved items.</Text> <Text style={styles.subHeader}>Pending Approval</Text>
)} {pendingVisible && (
</View> <AntDesign
); name="down"
size={17}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setPendingVisible(false);
}}
/>
)}
{!pendingVisible && (
<AntDesign
name="right"
size={15}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setPendingVisible(true);
}}
/>
)}
</View>
<View
centerV
style={{
aspectRatio: 1,
width: 35,
backgroundColor: "#faead2",
borderRadius: 50,
}}
>
<Text style={styles.counterNr} center color="#e28800">
{pendingGroceries?.length.toString()}
</Text>
</View>
</View>
{pendingGroceries?.length > 0
? pendingVisible && (
<FlatList
data={pendingGroceries}
renderItem={({item}) => (
<GroceryItem
item={item}
handleItemApproved={(id, changes) =>
updateGroceryItem({...changes, id: id})
}
/>
)}
keyExtractor={(item) => item.id.toString()}
/>
)
: pendingVisible && (
<Text style={styles.noItemTxt}>No items pending approval.</Text>
)}
{/* Approved Section */}
<View row spread marginT-40 marginB-0 centerV>
<View row centerV>
<Text style={styles.subHeader}>Shopping List</Text>
{approvedVisible && (
<AntDesign
name="down"
size={17}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setApprovedVisible(false);
}}
/>
)}
{!approvedVisible && (
<AntDesign
name="right"
size={15}
style={styles.dropIcon}
color="#9f9f9f"
onPress={() => {
setApprovedVisible(true);
}}
/>
)}
</View>
<View
centerV
style={{
aspectRatio: 1,
width: 35,
backgroundColor: "#e2eed8",
borderRadius: 50,
}}
>
<Text style={styles.counterNr} center color="#46a80a">
{approvedGroceries?.length.toString()}
</Text>
</View>
</View>
{isAddingGrocery && (
<View style={{marginTop: 8}}>
<EditGroceryItem
editGrocery={{
title: title,
setCategory: setCategory,
category: category,
setTitle: setTitle,
setSubmit: setSubmitted,
closeEdit: () => setIsAddingGrocery(false)
}}
/>
</View>
)}
{/* Render Approved Groceries Grouped by Category */}
{approvedGroceries?.length > 0
? approvedVisible && (
<FlatList
data={Object.keys(approvedGroceriesByCategory)}
renderItem={({item: 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})
}
/>
)
)}
</View>
)}
keyExtractor={(category) => category}
/>
)
: approvedVisible && (
<Text style={styles.noItemTxt}>No approved items.</Text>
)}
</View>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
dropIcon: { dropIcon: {
marginHorizontal: 10, marginHorizontal: 10,
}, },
noItemTxt: { noItemTxt: {
fontFamily: "Manrope_400Regular", fontFamily: "Manrope_400Regular",
fontSize: 14, fontSize: 14,
}, },
counterText: { counterText: {
fontSize: 14, fontSize: 14,
fontFamily: "PlusJakartaSans_600SemiBold", fontFamily: "PlusJakartaSans_600SemiBold",
}, },
subHeader: { subHeader: {
fontSize: 15, fontSize: 15,
fontFamily: "Manrope_700Bold", fontFamily: "Manrope_700Bold",
}, },
counterNr: { counterNr: {
fontFamily: "PlusJakartaSans_600SemiBold", fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 14 fontSize: 14
} }
}); });
export default GroceryList; export default GroceryList;

View File

@ -1,38 +1,38 @@
import { Text, ScrollView } from "react-native"; import {ScrollView} from "react-native";
import { View } from "react-native-ui-lib"; import {View} from "react-native-ui-lib";
import React, { useEffect, useRef } from "react"; import React, {useEffect, useRef} from "react";
import AddGroceryItem from "./AddGroceryItem"; import AddGroceryItem from "./AddGroceryItem";
import GroceryList from "./GroceryList"; import GroceryList from "./GroceryList";
import { useGroceryContext } from "@/contexts/GroceryContext"; import {useGroceryContext} from "@/contexts/GroceryContext";
const GroceryWrapper = () => { const GroceryWrapper = () => {
const { isAddingGrocery } = useGroceryContext(); const {isAddingGrocery} = useGroceryContext();
const scrollViewRef = useRef<ScrollView>(null); // Reference to the ScrollView const scrollViewRef = useRef<ScrollView>(null); // Reference to the ScrollView
useEffect(() => { useEffect(() => {
if (isAddingGrocery && scrollViewRef.current) { if (isAddingGrocery && scrollViewRef.current) {
scrollViewRef.current.scrollTo({ scrollViewRef.current.scrollTo({
y: 400, // Adjust this value to scroll a bit down (100 is an example) y: 400, // Adjust this value to scroll a bit down (100 is an example)
animated: true, animated: true,
}); });
} }
}, [isAddingGrocery]); }, [isAddingGrocery]);
return ( return (
<View height={"100%"} paddingT-15 paddingH-15> <View height={"100%"} paddingT-15 paddingH-15>
<View height={"100%"}> <View height={"100%"}>
<ScrollView <ScrollView
ref={scrollViewRef} // Assign the ref to the ScrollView ref={scrollViewRef}
automaticallyAdjustKeyboardInsets={true} automaticallyAdjustKeyboardInsets={true}
> >
<View marginB-70> <View marginB-70>
<GroceryList /> <GroceryList/>
</View> </View>
</ScrollView> </ScrollView>
{!isAddingGrocery && <AddGroceryItem />} {!isAddingGrocery && <AddGroceryItem/>}
</View> </View>
</View> </View>
); );
}; };
export default GroceryWrapper; export default GroceryWrapper;

View File

@ -1,170 +1,218 @@
import {Button, ButtonSize, Dialog, Text, TextField, View} from "react-native-ui-lib"; import {
import React, {useState} from "react"; Button,
import {useSignIn} from "@/hooks/firebase/useSignIn"; ButtonSize,
import {StyleSheet} from "react-native"; Dialog,
import Toast from 'react-native-toast-message'; Text,
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode"; TextField,
import {Camera, CameraView} from 'expo-camera'; TextFieldRef,
View,
} from "react-native-ui-lib";
import React, { useRef, useState } from "react";
import { useSignIn } from "@/hooks/firebase/useSignIn";
import { StyleSheet } from "react-native";
import Toast from "react-native-toast-message";
import { useLoginWithQrCode } from "@/hooks/firebase/useLoginWithQrCode";
import { Camera, CameraView } from "expo-camera";
const SignInPage = ({setTab}: { const SignInPage = ({
setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> setTab,
}: {
setTab: React.Dispatch<
React.SetStateAction<"register" | "login" | "reset-password">
>;
}) => { }) => {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const [hasPermission, setHasPermission] = useState<boolean | null>(null); const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false); const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const passwordRef = useRef<TextFieldRef>(null);
const {mutateAsync: signIn, error, isError} = useSignIn(); const { mutateAsync: signIn, error, isError } = useSignIn();
const {mutateAsync: signInWithQrCode} = useLoginWithQrCode() const { mutateAsync: signInWithQrCode } = useLoginWithQrCode();
const handleSignIn = async () => { const handleSignIn = async () => {
await signIn({email, password}); await signIn({ email, password });
if (!isError) { if (!isError) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Login successful!" text1: "Login successful!",
}); });
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",
text1: "Error logging in", text1: "Error logging in",
text2: `${error}` text2: `${error}`,
}); });
} }
}; };
const handleQrCodeScanned = async ({data}: { data: string }) => { const handleQrCodeScanned = async ({ data }: { data: string }) => {
setShowCameraDialog(false); setShowCameraDialog(false);
try { try {
await signInWithQrCode({userId: data}); await signInWithQrCode({ userId: data });
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Login successful with QR code!" text1: "Login successful with QR code!",
}); });
} catch (err) { } catch (err) {
Toast.show({ Toast.show({
type: "error", type: "error",
text1: "Error logging in with QR code", text1: "Error logging in with QR code",
text2: `${err}` text2: `${err}`,
}); });
} }
}; };
const getCameraPermissions = async (callback: () => void) => { const getCameraPermissions = async (callback: () => void) => {
const {status} = await Camera.requestCameraPermissionsAsync(); const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted'); setHasPermission(status === "granted");
if (status === 'granted') { if (status === "granted") {
callback(); callback();
} }
}; };
return ( return (
<View padding-10 centerV height={"100%"}> <View padding-10 centerV height={"100%"}>
<TextField <TextField
placeholder="Email" placeholder="Email"
value={email} value={email}
onChangeText={setEmail}
style={styles.textfield} onChangeText={setEmail}
/> style={styles.textfield}
<TextField onSubmitEditing={() => {
placeholder="Password" // Move focus to the description field
value={password} passwordRef.current?.focus();
onChangeText={setPassword} }}
secureTextEntry />
style={styles.textfield} <TextField
/> ref={passwordRef}
<Button placeholder="Password"
label="Login" value={password}
onPress={handleSignIn} onChangeText={setPassword}
style={{marginBottom: 20}} secureTextEntry
backgroundColor="#fd1775" style={styles.textfield}
/> />
<Button <Button
label="Login with a QR Code" label="Log in"
onPress={() => { marginT-50
getCameraPermissions(() => setShowCameraDialog(true)); labelStyle={{
} fontFamily: "PlusJakartaSans_600SemiBold",
} fontSize: 16,
style={{marginBottom: 20}} }}
backgroundColor="#fd1775" onPress={handleSignIn}
/> style={{ marginBottom: 20, height: 50 }}
{isError && <Text center style={{marginBottom: 20}}>{`${error?.toString()?.split("]")?.[1]}`}</Text>} backgroundColor="#fd1775"
/>
<Button
label="Log in with a QR Code"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={() => {
getCameraPermissions(() => setShowCameraDialog(true));
}}
style={{ marginBottom: 20, height: 50 }}
backgroundColor="#fd1775"
/>
{isError && (
<Text center style={{ marginBottom: 20 }}>{`${
error?.toString()?.split("]")?.[1]
}`}</Text>
)}
<View row centerH marginB-5 gap-5> <View row centerH marginB-5 gap-5>
<Text text70> <Text style={styles.jakartaLight}>Don't have an account?</Text>
Don't have an account? <Button
</Text> onPress={() => setTab("register")}
<Button label="Sign Up"
onPress={() => setTab("register")} labelStyle={[
label="Sign Up" styles.jakartaMedium,
link { textDecorationLine: "none", color: "#fd1575" },
size={ButtonSize.xSmall} ]}
padding-0 link
margin-0 size={ButtonSize.xSmall}
text70 padding-0
left margin-0
color="#fd1775" text70
/> left
</View> color="#fd1775"
/>
</View>
<View row centerH marginB-5 gap-5> <View row centerH marginB-5 gap-5>
<Text text70> <Text text70>Forgot your password?</Text>
Forgot your password? <Button
</Text> onPress={() => setTab("reset-password")}
<Button label="Reset password"
onPress={() => setTab("reset-password")} labelStyle={[
label="Reset password" styles.jakartaMedium,
link { textDecorationLine: "none", color: "#fd1575" },
size={ButtonSize.xSmall} ]}
padding-0 link
margin-0 size={ButtonSize.xSmall}
text70 padding-0
left margin-0
color="#fd1775" text70
/> left
</View> avoidInnerPadding
color="#fd1775"
/>
</View>
{/* Camera Dialog */} {/* Camera Dialog */}
<Dialog <Dialog
visible={showCameraDialog} visible={showCameraDialog}
onDismiss={() => setShowCameraDialog(false)} onDismiss={() => setShowCameraDialog(false)}
bottom bottom
width="100%" width="100%"
height="70%" height="70%"
containerStyle={{padding: 0}} containerStyle={{ padding: 0 }}
> >
{hasPermission === null ? ( {hasPermission === null ? (
<Text>Requesting camera permissions...</Text> <Text>Requesting camera permissions...</Text>
) : !hasPermission ? ( ) : !hasPermission ? (
<Text>No access to camera</Text> <Text>No access to camera</Text>
) : ( ) : (
<CameraView <CameraView
style={{flex: 1}} style={{ flex: 1 }}
onBarcodeScanned={handleQrCodeScanned} onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{ barcodeScannerSettings={{
barcodeTypes: ["qr"], barcodeTypes: ["qr"],
}} }}
/> />
)} )}
<Button <Button
label="Cancel" label="Cancel"
onPress={() => setShowCameraDialog(false)} onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775" backgroundColor="#fd1775"
style={{margin: 10}} style={{ margin: 10 }}
/> />
</Dialog> </Dialog>
</View> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
textfield: { textfield: {
backgroundColor: "white", backgroundColor: "white",
marginVertical: 10, marginVertical: 10,
padding: 30, padding: 30,
height: 45, height: 45,
borderRadius: 50, borderRadius: 50,
}, fontFamily: "PlusJakartaSans_300Light",
},
jakartaLight: {
fontFamily: "PlusJakartaSans_300Light",
fontSize: 16,
color: "#484848",
},
jakartaMedium: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
color: "#919191",
textDecorationLine: "underline",
},
}); });
export default SignInPage; export default SignInPage;

View File

@ -11,7 +11,7 @@ import {
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import { useSignUp } from "@/hooks/firebase/useSignUp"; import { useSignUp } from "@/hooks/firebase/useSignUp";
import { ProfileType } from "@/contexts/AuthContext"; import { ProfileType } from "@/contexts/AuthContext";
import { StyleSheet } from "react-native"; import { Dimensions, StyleSheet } from "react-native";
import { AntDesign } from "@expo/vector-icons"; import { AntDesign } from "@expo/vector-icons";
const SignUpPage = ({ const SignUpPage = ({
@ -40,19 +40,21 @@ const SignUpPage = ({
}; };
return ( return (
<View padding-10 height={"100%"} flexG> <View padding-15 marginT-30 height={Dimensions.get("window").height} flexG>
<Text text30 center> <Text style={styles.title}>Get started with Cally</Text>
Get started with Cally <Text style={styles.subtitle} marginT-15 color="#919191">
Please enter your details.
</Text> </Text>
<Text center>Please enter your details.</Text>
<TextField <TextField
marginT-60 marginT-30
autoFocus autoFocus
placeholder="First name" placeholder="First name"
value={firstName} value={firstName}
onChangeText={setFirstName} onChangeText={setFirstName}
style={styles.textfield} style={styles.textfield}
onSubmitEditing={() => {lnameRef.current?.focus()}} onSubmitEditing={() => {
lnameRef.current?.focus();
}}
blurOnSubmit={false} blurOnSubmit={false}
/> />
<TextField <TextField
@ -61,7 +63,9 @@ const SignUpPage = ({
value={lastName} value={lastName}
onChangeText={setLastName} onChangeText={setLastName}
style={styles.textfield} style={styles.textfield}
onSubmitEditing={() => {emailRef.current?.focus()}} onSubmitEditing={() => {
emailRef.current?.focus();
}}
blurOnSubmit={false} blurOnSubmit={false}
/> />
<TextField <TextField
@ -70,58 +74,69 @@ const SignUpPage = ({
value={email} value={email}
onChangeText={setEmail} onChangeText={setEmail}
style={styles.textfield} style={styles.textfield}
onSubmitEditing={() => {passwordRef.current?.focus()}} onSubmitEditing={() => {
passwordRef.current?.focus();
}}
blurOnSubmit={false} blurOnSubmit={false}
/> />
<TextField <View
ref={passwordRef} centerV
placeholder="Password" style={[styles.textfield, { padding: 0, paddingHorizontal: 30 }]}
value={password} >
onChangeText={setPassword} <TextField
secureTextEntry={!isPasswordVisible} ref={passwordRef}
style={styles.textfield} placeholder="Password"
trailingAccessory={ style={styles.jakartaLight}
<TouchableOpacity value={password}
onPress={() => setIsPasswordVisible(!isPasswordVisible)} onChangeText={setPassword}
> secureTextEntry={!isPasswordVisible}
<AntDesign trailingAccessory={
name={isPasswordVisible ? "eye" : "eyeo"} <TouchableOpacity
size={24} onPress={() => setIsPasswordVisible(!isPasswordVisible)}
color="gray" >
/> <AntDesign
</TouchableOpacity> name={isPasswordVisible ? "eye" : "eyeo"}
} size={24}
/> color="gray"
<View gap-10 marginH-10> />
</TouchableOpacity>
}
/>
</View>
<View gap-5 marginT-15>
<View row centerV> <View row centerV>
<Checkbox <Checkbox
style={[styles.check]}
color="#919191"
value={allowFaceID} value={allowFaceID}
onValueChange={(value) => { onValueChange={(value) => {
setAllowFaceID(value); setAllowFaceID(value);
}} }}
/> />
<Text text90R marginL-10> <Text style={styles.jakartaLight} marginL-10>
Allow FaceID for login in future Allow FaceID for login in future
</Text> </Text>
</View> </View>
<View row centerV> <View row centerV>
<Checkbox <Checkbox
style={styles.check}
color="#919191"
value={acceptTerms} value={acceptTerms}
onValueChange={(value) => setAcceptTerms(value)} onValueChange={(value) => setAcceptTerms(value)}
/> />
<View row> <View row>
<Text text90R marginL-10> <Text style={styles.jakartaLight} marginL-10>
I accept the I accept the
</Text> </Text>
<TouchableOpacity> <TouchableOpacity>
<Text text90 style={{ textDecorationLine: "underline" }}> <Text text90 style={styles.jakartaMedium}>
{" "} {" "}
terms and conditions terms and conditions
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<Text text90R> and </Text> <Text style={styles.jakartaLight}> and </Text>
<TouchableOpacity> <TouchableOpacity>
<Text text90 style={{ textDecorationLine: "underline" }}> <Text text90 style={styles.jakartaMedium}>
{" "} {" "}
privacy policy privacy policy
</Text> </Text>
@ -132,16 +147,24 @@ const SignUpPage = ({
<View style={styles.bottomView}> <View style={styles.bottomView}>
<Button <Button
label="Register" label="Register"
labelStyle={{
fontFamily: "PlusJakartaSans_600SemiBold",
fontSize: 16,
}}
onPress={handleSignUp} onPress={handleSignUp}
style={{ marginBottom: 10, backgroundColor: "#fd1775" }} style={{ marginBottom: 0, backgroundColor: "#fd1775", height: 50 }}
/> />
<View row centerH marginT-10 marginB-5 gap-5> <View row centerH marginT-10 marginB-2 gap-5>
<Text text70 center> <Text style={[styles.jakartaLight, { fontSize: 16, color: "#484848" }]} center>
Already have an account? Already have an account?
</Text> </Text>
<Button <Button
label="Sign In" label="Log in"
labelStyle={[
styles.jakartaMedium,
{ fontSize: 16, textDecorationLine: "none", color: "#fd1775" },
]}
flexS flexS
margin-0 margin-0
link link
@ -161,11 +184,35 @@ export default SignUpPage;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
textfield: { textfield: {
backgroundColor: "white", backgroundColor: "white",
marginVertical: 10, marginVertical: 8,
padding: 30, padding: 30,
height: 45, height: 44,
borderRadius: 50, borderRadius: 50,
fontFamily: "PlusJakartaSans_300Light",
fontSize: 13,
color: "#919191",
}, },
//mora da se izmeni kako treba //mora da se izmeni kako treba
bottomView: { marginTop: 150 }, bottomView: { marginTop: "auto", marginBottom: 30 },
jakartaLight: {
fontFamily: "PlusJakartaSans_300Light",
fontSize: 13,
color: "#919191",
},
jakartaMedium: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13,
color: "#919191",
textDecorationLine: "underline",
},
title: { fontFamily: "Manrope_600SemiBold", fontSize: 34, marginTop: 50 },
subtitle: { fontFamily: "PlusJakartaSans_400Regular", fontSize: 16 },
check: {
borderRadius: 3,
aspectRatio: 1,
width: 18,
color: "#919191",
borderColor: "#919191",
borderWidth: 1,
},
}); });

View File

@ -1,338 +1,353 @@
import { View, Text, Button, Switch, PickerModes } from "react-native-ui-lib";
import React, { useRef, useState } from "react";
import PointsSlider from "@/components/shared/PointsSlider";
import { repeatOptions, useToDosContext } from "@/contexts/ToDosContext";
import { Feather, AntDesign, Ionicons } from "@expo/vector-icons";
import { import {
Dialog, Button,
TextField, ButtonSize,
DateTimePicker, DateTimePicker,
Picker, Dialog,
ButtonSize, Picker,
PickerModes,
Switch,
Text,
TextField,
TextFieldRef,
View
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView"; import React, {useEffect, useRef, useState} from "react";
import { Dimensions, StyleSheet } from "react-native"; import PointsSlider from "@/components/shared/PointsSlider";
import {repeatOptions, useToDosContext} from "@/contexts/ToDosContext";
import {Ionicons} from "@expo/vector-icons";
import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView";
import {Dimensions, KeyboardAvoidingView, StyleSheet} from "react-native";
import DropModalIcon from "@/assets/svgs/DropModalIcon"; import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { IToDo } from "@/hooks/firebase/types/todoData"; import {IToDo} from "@/hooks/firebase/types/todoData";
import AssigneesDisplay from "@/components/shared/AssigneesDisplay"; import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers"; import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import CalendarIcon from "@/assets/svgs/CalendarIcon"; import CalendarIcon from "@/assets/svgs/CalendarIcon";
import ClockIcon from "@/assets/svgs/ClockIcon";
import ClockOIcon from "@/assets/svgs/ClockOIcon"; import ClockOIcon from "@/assets/svgs/ClockOIcon";
import ProfileIcon from "@/assets/svgs/ProfileIcon"; import ProfileIcon from "@/assets/svgs/ProfileIcon";
import RepeatFreq from "./RepeatFreq"; import RepeatFreq from "./RepeatFreq";
interface IAddChoreDialog { interface IAddChoreDialog {
isVisible: boolean; isVisible: boolean;
setIsVisible: (value: boolean) => void; setIsVisible: (value: boolean) => void;
selectedTodo?: IToDo; selectedTodo?: IToDo;
} }
const defaultTodo = { const defaultTodo = {
id: "", id: "",
title: "", title: "",
points: 10, points: 10,
date: new Date(), date: new Date(),
rotate: false, rotate: false,
repeatType: "Every week", repeatType: "Every week",
assignees: [], assignees: [],
repeatDays: [] repeatDays: []
}; };
const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => { const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
const { addToDo, updateToDo } = useToDosContext(); const {addToDo, updateToDo} = useToDosContext();
const [todo, setTodo] = useState<IToDo>( const [todo, setTodo] = useState<IToDo>(
addChoreDialogProps.selectedTodo ?? defaultTodo addChoreDialogProps.selectedTodo ?? defaultTodo
); );
const [selectedAssignees, setSelectedAssignees] = useState<string[]>( const [selectedAssignees, setSelectedAssignees] = useState<string[]>(
addChoreDialogProps?.selectedTodo?.assignees ?? [] addChoreDialogProps?.selectedTodo?.assignees ?? []
); );
const { width, height } = Dimensions.get("screen"); const {width} = Dimensions.get("screen");
const [points, setPoints] = useState<number>(todo.points); const [points, setPoints] = useState<number>(todo.points);
const { data: members } = useGetFamilyMembers(); const titleRef = useRef<TextFieldRef>(null)
const handleClose = () => { const {data: members} = useGetFamilyMembers();
setTodo(defaultTodo);
setSelectedAssignees([]);
addChoreDialogProps.setIsVisible(false);
};
const handleChange = (text: string) => { const handleClose = () => {
const numericValue = parseInt(text, 10); setTodo(defaultTodo);
setSelectedAssignees([]);
addChoreDialogProps.setIsVisible(false);
};
if (!isNaN(numericValue) && numericValue >= 0 && numericValue <= 100) { const handleChange = (text: string) => {
setPoints(numericValue); const numericValue = parseInt(text, 10);
} else if (text === "") {
setPoints(0);
}
};
const handleRepeatDaysChange = (day: string, set: boolean) => { if (!isNaN(numericValue) && numericValue >= 0 && numericValue <= 100) {
if (set) { setPoints(numericValue);
const updatedTodo = { } else if (text === "") {
...todo, setPoints(0);
repeatDays: [...todo.repeatDays, day] }
} };
setTodo(updatedTodo);
} else {
const array = todo.repeatDays ?? [];
let index = array.indexOf(day);
array.splice(index, 1);
const updatedTodo = {
...todo,
repeatDays: array
}
setTodo(updatedTodo);
}
}
return ( const handleRepeatDaysChange = (day: string, set: boolean) => {
<Dialog if (set) {
bottom={true} const updatedTodo = {
height={"90%"} ...todo,
width={width} repeatDays: [...todo.repeatDays, day]
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => handleClose}
containerStyle={{
borderRadius: 10,
backgroundColor: "white",
alignSelf: "stretch",
padding: 0,
paddingTop: 4,
margin: 0,
}}
visible={addChoreDialogProps.isVisible}
>
<View row spread>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Cancel"
onPress={() => {
handleClose();
}}
/>
<View marginT-12>
<DropModalIcon
onPress={() => {
handleClose();
}}
/>
</View>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Save"
onPress={() => {
try {
if (addChoreDialogProps.selectedTodo) {
updateToDo({
...todo,
points: points,
assignees: selectedAssignees
});
} else {
addToDo({
...todo,
done: false,
points: points,
assignees: selectedAssignees,
repeatDays: todo.repeatDays ?? []
});
}
handleClose();
} catch (error) {
console.error(error);
} }
}} setTodo(updatedTodo);
/> } else {
</View> const array = todo.repeatDays ?? [];
<TextField let index = array.indexOf(day);
placeholder="Add a To Do" array.splice(index, 1);
autoFocus const updatedTodo = {
value={todo?.title} ...todo,
onChangeText={(text) => { repeatDays: array
setTodo((oldValue: IToDo) => ({ ...oldValue, title: text })); }
}} setTodo(updatedTodo);
placeholderTextColor="#2d2d30" }
text60R }
marginT-15
marginL-30
/>
<View style={styles.divider} marginT-8 />
<View marginL-30 centerV>
<View row marginB-10>
{todo?.date && (
<View row centerV>
<CalendarIcon color="#919191" width={24} height={24} />
<DateTimePicker
value={todo.date}
text70
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-12
onChange={(date) => {
setTodo((oldValue: IToDo) => ({ ...oldValue, date: date }));
}}
/>
</View>
)}
</View>
<View row centerV>
<ClockOIcon />
<Picker
marginL-12
placeholder="Select Repeat Type"
value={todo?.repeatType}
onChange={(value) => {
if (value) {
if (value.toString() == "None") {
setTodo((oldValue) => ({
...oldValue,
date: null,
repeatType: "None",
}));
} else {
setTodo((oldValue) => ({
...oldValue,
date: new Date(),
repeatType: value.toString(),
}));
}
}
}}
topBarProps={{ title: "Repeat" }}
style={{
marginVertical: 5,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
>
{repeatOptions.map((option) => (
<Picker.Item
key={option.value}
label={option.label}
value={option.value}
/>
))}
</Picker>
</View>
{todo.repeatType == "Every week" && <RepeatFreq handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={todo.repeatDays ?? []}/>}
</View>
<View style={styles.divider} />
<View marginH-30 marginB-10 row centerV> useEffect(() => {
<ProfileIcon color="#919191" /> setTimeout(() => {
<Text style={styles.sub} marginL-10> titleRef?.current?.focus()
Assignees }, 500)
</Text> }, []);
<View flex-1 />
<Picker return (
marginL-8 <Dialog
value={selectedAssignees} bottom={true}
onChange={(value) => { height={"90%"}
setSelectedAssignees([...selectedAssignees, ...value]); width={width}
}} panDirection={PanningDirectionsEnum.DOWN}
style={{ marginVertical: 5 }} onDismiss={() => handleClose}
mode={PickerModes.MULTI} containerStyle={{
renderInput={() => ( borderRadius: 10,
<Button backgroundColor: "white",
size={ButtonSize.small} alignSelf: "stretch",
paddingH-8 padding: 0,
iconSource={() => ( paddingTop: 4,
<Ionicons name="add-outline" size={20} color="#ea156c" /> margin: 0,
)} }}
style={{ visible={addChoreDialogProps.isVisible}
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
color="#ea156c"
label="Assign"
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
/>
)}
> >
{members?.map((member) => ( <View row spread>
<Picker.Item <Button
key={member.uid} color="#05a8b6"
label={member?.firstName + " " + member?.lastName} style={styles.topBtn}
value={member?.uid!} label="Cancel"
/> onPress={() => {
))} handleClose();
</Picker> }}
</View> />
<View row marginL-27 marginT-0> <View marginT-12>
<AssigneesDisplay <DropModalIcon
selectedAttendees={selectedAssignees} onPress={() => {
setSelectedAttendees={setSelectedAssignees} handleClose();
/> }}
</View> />
<View row centerV style={styles.rotateSwitch}> </View>
<Text style={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 16 }}> <Button
Take Turns color="#05a8b6"
</Text> style={styles.topBtn}
<Switch label="Save"
onColor={"#ea156c"} onPress={() => {
value={todo.rotate} try {
style={{ width: 43.06, height: 27.13 }} if (addChoreDialogProps.selectedTodo) {
marginL-10 updateToDo({
onValueChange={(value) => ...todo,
setTodo((oldValue) => ({ ...oldValue, rotate: value })) points: points,
} assignees: selectedAssignees
/> });
</View> } else {
<View style={styles.divider} /> addToDo({
<View marginH-30 marginB-15 row centerV> ...todo,
<Ionicons name="gift-outline" size={25} color="#919191" /> done: false,
<Text style={styles.sub} marginL-10> points: points,
Reward Points assignees: selectedAssignees,
</Text> repeatDays: todo.repeatDays ?? []
</View> });
<PointsSlider }
points={points} handleClose();
setPoints={setPoints} } catch (error) {
handleChange={handleChange} console.error(error);
/> }
</Dialog> }}
); />
</View>
<KeyboardAvoidingView>
<TextField
placeholder="Add a To Do"
value={todo?.title}
onChangeText={(text) => {
setTodo((oldValue: IToDo) => ({...oldValue, title: text}));
}}
placeholderTextColor="#2d2d30"
text60R
marginT-15
marginL-30
ref={titleRef}
/>
<View style={styles.divider} marginT-8/>
<View marginL-30 centerV>
<View row marginB-10>
{todo?.date && (
<View row centerV>
<CalendarIcon color="#919191" width={24} height={24}/>
<DateTimePicker
value={todo.date}
text70
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-12
onChange={(date) => {
setTodo((oldValue: IToDo) => ({...oldValue, date: date}));
}}
/>
</View>
)}
</View>
<View row centerV>
<ClockOIcon/>
<Picker
marginL-12
placeholder="Select Repeat Type"
value={todo?.repeatType}
onChange={(value) => {
if (value) {
if (value.toString() == "None") {
setTodo((oldValue) => ({
...oldValue,
date: null,
repeatType: "None",
}));
} else {
setTodo((oldValue) => ({
...oldValue,
date: new Date(),
repeatType: value.toString(),
}));
}
}
}}
topBarProps={{title: "Repeat"}}
style={{
marginVertical: 5,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
>
{repeatOptions.map((option) => (
<Picker.Item
key={option.value}
label={option.label}
value={option.value}
/>
))}
</Picker>
</View>
{todo.repeatType == "Every week" && <RepeatFreq handleRepeatDaysChange={handleRepeatDaysChange}
repeatDays={todo.repeatDays ?? []}/>}
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-10 row centerV>
<ProfileIcon color="#919191"/>
<Text style={styles.sub} marginL-10>
Assignees
</Text>
<View flex-1/>
<Picker
marginL-8
value={selectedAssignees}
onChange={(value) => {
setSelectedAssignees([...selectedAssignees, ...value]);
}}
style={{marginVertical: 5}}
mode={PickerModes.MULTI}
renderInput={() => (
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c"/>
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
color="#ea156c"
label="Assign"
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
/>
)}
>
{members?.map((member) => (
<Picker.Item
key={member.uid}
label={member?.firstName + " " + member?.lastName}
value={member?.uid!}
/>
))}
</Picker>
</View>
<View row marginL-27 marginT-0>
<AssigneesDisplay
selectedAttendees={selectedAssignees}
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>
<View style={styles.divider}/>
<View marginH-30 marginB-15 row centerV>
<Ionicons name="gift-outline" size={25} color="#919191"/>
<Text style={styles.sub} marginL-10>
Reward Points
</Text>
</View>
<PointsSlider
points={points}
setPoints={setPoints}
handleChange={handleChange}
/>
</KeyboardAvoidingView>
</Dialog>
);
}; };
export default AddChoreDialog; export default AddChoreDialog;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 }, divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
gradient: { gradient: {
height: "25%", height: "25%",
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
width: "100%", width: "100%",
}, },
buttonContainer: { buttonContainer: {
position: "absolute", position: "absolute",
bottom: 25, bottom: 25,
width: "100%", width: "100%",
}, },
button: { button: {
backgroundColor: "rgb(253, 23, 117)", backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20, paddingVertical: 20,
}, },
topBtn: { topBtn: {
backgroundColor: "white", backgroundColor: "white",
color: "#05a8b6", color: "#05a8b6",
}, },
rotateSwitch: { rotateSwitch: {
marginLeft: 35, marginLeft: 35,
marginBottom: 10, marginBottom: 10,
marginTop: 25, marginTop: 25,
}, },
sub: { sub: {
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",
fontSize: 18, fontSize: 18,
}, },
}); });

View File

@ -1,19 +1,27 @@
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import firestore from "@react-native-firebase/firestore"; import firestore from "@react-native-firebase/firestore";
import { useAuthContext } from "@/contexts/AuthContext"; import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import {IToDo} from "@/hooks/firebase/types/todoData"; import { IToDo } from "@/hooks/firebase/types/todoData";
export const useGetTodos = () => { export const useGetTodos = () => {
const { user, profileData } = useAuthContext(); const { user, profileData } = useAuthContext();
//TODO: Add role based filtering for todos
return useQuery({ return useQuery({
queryKey: ["todos", user?.uid], queryKey: ["todos", user?.uid],
queryFn: async () => { queryFn: async () => {
const snapshot = await firestore()
.collection("Todos") let snapshot;
.where("familyId", "==", profileData?.familyId) if (profileData?.userType === ProfileType.PARENT) {
.get(); snapshot = await firestore()
.collection("Todos")
.where("familyId", "==", profileData?.familyId)
.get();
} else {
snapshot = await firestore()
.collection("Todos")
.where("assignees", "array-contains", user?.uid)
.get();
}
return snapshot.docs.map((doc) => { return snapshot.docs.map((doc) => {
const data = doc.data(); const data = doc.data();

View File

@ -127,6 +127,7 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>SplashScreen</string> <string>SplashScreen</string>