From d2b46ad97747bd0b415fdaaee50ac102980beaba Mon Sep 17 00:00:00 2001 From: ivic00 <102467664+ivic00@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:57:41 +0200 Subject: [PATCH 1/9] added focus, ui changes --- assets/svgs/ArrowRightIcon.tsx | 19 + assets/svgs/CircledXIcon.tsx | 22 + assets/svgs/EmailIcon.tsx | 17 + assets/svgs/QRIcon.tsx | 20 + components/pages/brain_dump/AddBrainDump.tsx | 74 +- components/pages/brain_dump/MoveBrainDump.tsx | 11 +- .../pages/calendar/ManuallyAddEventModal.tsx | 2 + components/pages/grocery/CategoryDropdown.tsx | 2 +- components/pages/grocery/EditGroceryItem.tsx | 4 +- components/pages/main/SignUpPage.tsx | 25 +- components/pages/settings/SettingsPage.tsx | 141 +-- .../settings/user_settings_views/MyGroup.tsx | 841 ++++++++++-------- .../settings/user_settings_views/UserMenu.tsx | 2 +- components/pages/todos/AddChoreDialog.tsx | 25 +- 14 files changed, 729 insertions(+), 476 deletions(-) create mode 100644 assets/svgs/ArrowRightIcon.tsx create mode 100644 assets/svgs/CircledXIcon.tsx create mode 100644 assets/svgs/EmailIcon.tsx create mode 100644 assets/svgs/QRIcon.tsx diff --git a/assets/svgs/ArrowRightIcon.tsx b/assets/svgs/ArrowRightIcon.tsx new file mode 100644 index 0000000..37e85cf --- /dev/null +++ b/assets/svgs/ArrowRightIcon.tsx @@ -0,0 +1,19 @@ +import * as React from "react" +import Svg, { SvgProps, Path } from "react-native-svg" +const ArrowRightIcon = (props: SvgProps) => ( + + + +) +export default ArrowRightIcon diff --git a/assets/svgs/CircledXIcon.tsx b/assets/svgs/CircledXIcon.tsx new file mode 100644 index 0000000..9759ef8 --- /dev/null +++ b/assets/svgs/CircledXIcon.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import Svg, { SvgProps, Path } from "react-native-svg"; +const CircledXIcon = (props: SvgProps) => ( + + + + +); +export default CircledXIcon; diff --git a/assets/svgs/EmailIcon.tsx b/assets/svgs/EmailIcon.tsx new file mode 100644 index 0000000..47d6e00 --- /dev/null +++ b/assets/svgs/EmailIcon.tsx @@ -0,0 +1,17 @@ +import * as React from "react" +import Svg, { SvgProps, Path } from "react-native-svg" +const EmailIcon = (props: SvgProps) => ( + + + +) +export default EmailIcon diff --git a/assets/svgs/QRIcon.tsx b/assets/svgs/QRIcon.tsx new file mode 100644 index 0000000..f7be63d --- /dev/null +++ b/assets/svgs/QRIcon.tsx @@ -0,0 +1,20 @@ +import * as React from "react" +import Svg, { SvgProps, Path } from "react-native-svg" +const QRIcon = (props: SvgProps) => ( + + + +) +export default QRIcon diff --git a/components/pages/brain_dump/AddBrainDump.tsx b/components/pages/brain_dump/AddBrainDump.tsx index 00bc3f2..a134f5d 100644 --- a/components/pages/brain_dump/AddBrainDump.tsx +++ b/components/pages/brain_dump/AddBrainDump.tsx @@ -1,17 +1,24 @@ -import { View, Text, Button, TextField } from "react-native-ui-lib"; -import React, { useEffect, useState } from "react"; +import { + View, + Text, + Button, + TextField, + TextFieldRef, + TouchableOpacity, +} from "react-native-ui-lib"; +import React, { useEffect, useState, useRef } from "react"; import { Dialog } from "react-native-ui-lib"; import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView"; -import CloseXIcon from "@/assets/svgs/CloseXIcon"; -import { Dimensions, StyleSheet } from "react-native"; +import { Dimensions, Keyboard, StyleSheet } from "react-native"; + import DropModalIcon from "@/assets/svgs/DropModalIcon"; -import MenuIcon from "@/assets/svgs/MenuIcon"; import { useBrainDumpContext } from "@/contexts/DumpContext"; interface IAddBrainDumpProps { isVisible: boolean; setIsVisible: (value: boolean) => void; } + const AddBrainDump = ({ addBrainDumpProps, }: { @@ -20,7 +27,10 @@ const AddBrainDump = ({ const { addBrainDump } = useBrainDumpContext(); const [dumpTitle, setDumpTitle] = useState(""); const [dumpDesc, setDumpDesc] = useState(""); - const { width, height } = Dimensions.get("screen"); + const { width } = Dimensions.get("screen"); + + // Refs for the two TextFields + const descriptionRef = useRef(null); useEffect(() => { setDumpDesc(""); @@ -34,14 +44,7 @@ const AddBrainDump = ({ width={width} panDirection={PanningDirectionsEnum.DOWN} onDismiss={() => addBrainDumpProps.setIsVisible(false)} - containerStyle={{ - borderTopRightRadius: 15, - borderTopLeftRadius: 15, - backgroundColor: "white", - padding: 0, - paddingTop: 3, - margin: 0, - }} + containerStyle={styles.dialogContainer} visible={addBrainDumpProps.isVisible} > @@ -53,16 +56,15 @@ const AddBrainDump = ({ addBrainDumpProps.setIsVisible(false); }} /> - addBrainDumpProps.setIsVisible(false)} - /> + addBrainDumpProps.setIsVisible(false)}> + + - - - setShowAddUserDialog(false)} center> - Return to user settings - - - - - setShowNewUserInfoDialog(false)} - > - - - New User Information - setShowAddUserDialog(false)}> - X - - - - - - { - }}> - - Upload User Profile Photo - - - - - Member Status - setSelectedStatus(item)} - style={styles.picker} - showSearch - floatingPlaceholder - > - - - - - - - - {selectedStatus === ProfileType.FAMILY_DEVICE ? "Device Name" : "First Name"} + {(!!parents.length || !!children.length) && ( + <> + + Family + + {[...parents, ...children]?.map((member, index) => ( + + + + + {member.firstName} {member.lastName} - + + {member.userType === ProfileType.PARENT + ? "Admin (You)" + : "Child"} + + - {selectedStatus !== ProfileType.FAMILY_DEVICE && ( - <> - Last Name - - - )} + - {selectedStatus !== ProfileType.FAMILY_DEVICE && ( - <> - Email Address (Optional) - - - )} - - - - ); + ))} + + )} + + {!!caregivers.length && ( + <> + + Caregivers + + {caregivers?.map((member) => ( + + + + + {member.firstName} {member.lastName} + + + Caregiver + + + + setShowQRCodeDialog("")} + showQRCodeDialog={showQRCodeDialog === member?.uid} + userId={member?.uid!} + /> + + ))} + + )} + + {!!familyDevices.length && ( + <> + + Family Devices + + {familyDevices?.map((member, index) => ( + + + + {member.firstName} + + Family Device + + + + setShowQRCodeDialog("")} + showQRCodeDialog={showQRCodeDialog === member?.uid} + userId={member?.uid!} + /> + + ))} + + )} + + + + setShowAddUserDialog(true), + style: styles.bottomButton, + }} + /> + + setShowAddUserDialog(false)} + panDirection={PanningProvider.Directions.DOWN} + > + + + Add a new user device + + + + + + setShowAddUserDialog(false)} + center + marginT-30 + > + Return to user settings + + + + + setShowNewUserInfoDialog(false)} + > + + + + New User Information + + {setShowNewUserInfoDialog(false)}}> + + + + + + + + } + backgroundColor={Colors.grey60} + style={{ borderRadius: 25 }} + center + /> + {}}> + + Upload User Profile Photo + + + + + Member Status + setSelectedStatus(item)} + style={styles.picker} + showSearch + floatingPlaceholder + > + + + + + + + + {selectedStatus === ProfileType.FAMILY_DEVICE + ? "Device Name" + : "First Name"} + + {lNameRef.current?.focus()}} + blurOnSubmit={false} + /> + + {selectedStatus !== ProfileType.FAMILY_DEVICE && ( + <> + Last Name + {emailRef.current?.focus()}} + blurOnSubmit={false} + /> + + )} + + {selectedStatus !== ProfileType.FAMILY_DEVICE && ( + <> + Email Address (Optional) + + + )} + + + + ); }; const styles = StyleSheet.create({ - card: { - marginVertical: 15, - backgroundColor: "white", - width: "100%", - borderRadius: 15, - padding: 20, - }, - familyCard: { - marginBottom: 10, - borderRadius: 10, - backgroundColor: Colors.white, - width: "100%", - }, - inputField: { - borderRadius: 50, - paddingVertical: 12, - paddingHorizontal: 16, - backgroundColor: Colors.grey80, - marginBottom: 16, - borderColor: Colors.grey50, - borderWidth: 1, - height: 40, - }, - picker: { - borderRadius: 50, - paddingVertical: 12, - paddingHorizontal: 16, - backgroundColor: Colors.grey80, - marginBottom: 16, - borderColor: Colors.grey50, - borderWidth: 1, - marginTop: -20, - height: 40, - }, - label: { - marginBottom: 5, - fontSize: 12, - color: Colors.grey40, - }, - dialogCard: { - borderRadius: 10, - gap: 10, - }, - subTit: { - fontFamily: "Manrope_500Medium", - fontSize: 15, - }, + dialogBtn: { + height: 47, + width: 279, + }, + dialogTitle: { fontFamily: "Manrope_600SemiBold", fontSize: 22 }, + dialogBackBtn: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 15, + color: "#a7a7a7", + }, + card: { + marginVertical: 15, + backgroundColor: "white", + width: "100%", + borderRadius: 15, + padding: 20, + }, + bottomButton: { + position: "absolute", + bottom: 80, + width: "100%", + }, + familyCard: { + marginBottom: 10, + borderRadius: 10, + backgroundColor: Colors.white, + width: "100%", + }, + inputField: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 13, + color: "#565656", + borderRadius: 50, + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: Colors.grey80, + marginBottom: 16, + borderColor: Colors.grey50, + borderWidth: 1, + height: 40, + }, + picker: { + borderRadius: 50, + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: Colors.grey80, + marginBottom: 16, + borderColor: Colors.grey50, + borderWidth: 1, + marginTop: -20, + height: 40, + }, + label: { + marginBottom: 5, + fontSize: 12, + color: Colors.grey40, + }, + dialogCard: { + borderRadius: 10, + gap: 10, + }, + subTit: { + fontFamily: "Manrope_500Medium", + fontSize: 15, + }, + dialogBtnLbl: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 15, + color: "white", + }, + divider: { height: 0.7, backgroundColor: "#e6e6e6", width: "100%" }, + jakarta12: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 12, + color: "#a1a1a1", + }, + jakarta13: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 13, + }, }); export default MyGroup; diff --git a/components/pages/settings/user_settings_views/UserMenu.tsx b/components/pages/settings/user_settings_views/UserMenu.tsx index ed01179..74f4bfc 100644 --- a/components/pages/settings/user_settings_views/UserMenu.tsx +++ b/components/pages/settings/user_settings_views/UserMenu.tsx @@ -30,7 +30,7 @@ const UserMenu = ({ customContent={ - Show Login QR Code + Show Login QR Code } diff --git a/components/pages/todos/AddChoreDialog.tsx b/components/pages/todos/AddChoreDialog.tsx index 8dc80c1..c8f061c 100644 --- a/components/pages/todos/AddChoreDialog.tsx +++ b/components/pages/todos/AddChoreDialog.tsx @@ -1,5 +1,5 @@ import { View, Text, Button, Switch } from "react-native-ui-lib"; -import React, { useState } from "react"; +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"; @@ -14,6 +14,7 @@ import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView import { Dimensions, StyleSheet } from "react-native"; import DropModalIcon from "@/assets/svgs/DropModalIcon"; import { IToDo } from "@/hooks/firebase/types/todoData"; +import AssigneesDisplay from "@/components/shared/AssigneesDisplay"; interface IAddChoreDialog { isVisible: boolean; @@ -111,6 +112,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => { { setTodo((oldValue: IToDo) => ({ ...oldValue, title: text })); @@ -197,25 +199,8 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => { label="Assign" /> - - - + + Take Turns From 3f7fc92952cbe1f2fecac791a1e70331d6d871c2 Mon Sep 17 00:00:00 2001 From: Milan Paunovic Date: Sat, 19 Oct 2024 17:20:34 +0200 Subject: [PATCH 2/9] Calendar page refactor --- app/(auth)/calendar/index.tsx | 15 +- app/_layout.tsx | 466 +++++---- components/pages/calendar/AddEventDialog.tsx | 322 +++---- components/pages/calendar/CalendarHeader.tsx | 102 ++ components/pages/calendar/CalendarPage.tsx | 211 +---- .../pages/calendar/CalendarViewSwitch.tsx | 156 +-- components/pages/calendar/EditEventDialog.tsx | 581 ++++++------ components/pages/calendar/EventCalendar.tsx | 63 ++ components/pages/calendar/InnerCalendar.tsx | 41 + .../pages/calendar/ManuallyAddEventModal.tsx | 896 +++++++++--------- components/pages/calendar/atoms.ts | 9 + components/pages/calendar/constants.ts | 20 + components/pages/calendar/interfaces.ts | 13 + components/pages/main/SignInPage.tsx | 30 +- components/pages/settings/SettingsPage.tsx | 200 ++-- contexts/AuthContext.tsx | 1 + contexts/CalendarContext.tsx | 194 ---- hooks/firebase/types/eventData.ts | 2 +- hooks/firebase/useGetEvents.ts | 5 +- hooks/firebase/useUpdateEvent.ts | 4 +- ios/cally.xcodeproj/project.pbxproj | 4 +- 21 files changed, 1580 insertions(+), 1755 deletions(-) create mode 100644 components/pages/calendar/CalendarHeader.tsx create mode 100644 components/pages/calendar/EventCalendar.tsx create mode 100644 components/pages/calendar/InnerCalendar.tsx create mode 100644 components/pages/calendar/atoms.ts create mode 100644 components/pages/calendar/constants.ts create mode 100644 components/pages/calendar/interfaces.ts delete mode 100644 contexts/CalendarContext.tsx diff --git a/app/(auth)/calendar/index.tsx b/app/(auth)/calendar/index.tsx index 4c4da19..ba50b83 100644 --- a/app/(auth)/calendar/index.tsx +++ b/app/(auth)/calendar/index.tsx @@ -1,14 +1,11 @@ import React from "react"; -import { CalendarProvider } from "@/contexts/CalendarContext"; // Import the new CalendarPage component import CalendarPage from "@/components/pages/calendar/CalendarPage"; -import { SettingsContextProvider } from "@/contexts/SettingsContext"; +import {SettingsContextProvider} from "@/contexts/SettingsContext"; export default function Screen() { - return ( - - - - - - ); + return ( + + + + ); } diff --git a/app/_layout.tsx b/app/_layout.tsx index 42e1941..9b2e5f8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,170 +1,6 @@ -import React, { useEffect } from "react"; -import { DefaultTheme, ThemeProvider } from "@react-navigation/native"; +import React, {useEffect} from "react"; +import {DefaultTheme, ThemeProvider} from "@react-navigation/native"; import { - useFonts, - Manrope_200ExtraLight, - Manrope_300Light, - Manrope_400Regular, - Manrope_500Medium, - Manrope_600SemiBold, - Manrope_700Bold, - Manrope_800ExtraBold, -} from "@expo-google-fonts/manrope"; -import { - PlusJakartaSans_200ExtraLight, - PlusJakartaSans_300Light, - PlusJakartaSans_400Regular, - PlusJakartaSans_500Medium, - PlusJakartaSans_600SemiBold, - PlusJakartaSans_700Bold, - PlusJakartaSans_800ExtraBold, - PlusJakartaSans_200ExtraLight_Italic, - PlusJakartaSans_300Light_Italic, - PlusJakartaSans_400Regular_Italic, - PlusJakartaSans_500Medium_Italic, - PlusJakartaSans_600SemiBold_Italic, - PlusJakartaSans_700Bold_Italic, - PlusJakartaSans_800ExtraBold_Italic, -} from "@expo-google-fonts/plus-jakarta-sans"; -import { - Poppins_100Thin, - Poppins_100Thin_Italic, - Poppins_200ExtraLight, - Poppins_200ExtraLight_Italic, - Poppins_300Light, - Poppins_300Light_Italic, - Poppins_400Regular, - Poppins_400Regular_Italic, - Poppins_500Medium, - Poppins_500Medium_Italic, - Poppins_600SemiBold, - Poppins_600SemiBold_Italic, - Poppins_700Bold, - Poppins_700Bold_Italic, - Poppins_800ExtraBold, - Poppins_800ExtraBold_Italic, - Poppins_900Black, - Poppins_900Black_Italic, -} from "@expo-google-fonts/poppins"; -import { Stack } from "expo-router"; -import * as SplashScreen from "expo-splash-screen"; -import "react-native-reanimated"; -import { AuthContextProvider } from "@/contexts/AuthContext"; -import { QueryClient, QueryClientProvider } from "react-query"; -import { - ThemeManager, - Typography, - Toast, - TextProps, -} from "react-native-ui-lib"; -import functions from "@react-native-firebase/functions"; -import auth from "@react-native-firebase/auth"; -import firestore from "@react-native-firebase/firestore"; - -SplashScreen.preventAutoHideAsync(); - -const queryClient = new QueryClient(); - -if (__DEV__) { - functions().useEmulator("localhost", 5001); - firestore().useEmulator("localhost", 5471); - auth().useEmulator("http://localhost:9099"); -} - -type TextStyleBase = - | "text10" - | "text20" - | "text30" - | "text40" - | "text50" - | "text60" - | "text70" - | "text80" - | "text90" - | "text100"; -type TextStyleModifier = "R" | "M" | "BO" | "H" | "BL" | "L"; -type TextStyle = TextStyleBase | `${TextStyleBase}${TextStyleModifier}`; - -type TextStyleProps = { - [K in TextStyle]?: boolean; -}; - -type ExtendedTextProps = TextProps & TextStyleProps; - -interface FontStyle { - fontFamily: string; - fontSize: number; -} - -const getManropeFontStyle = (style: TextStyle): FontStyle => { - let fontFamily: string; - let fontSize: number; - - if (style.includes("L") || style.includes("BL")) - fontFamily = "Manrope_300Light"; - else if (style.includes("R")) fontFamily = "Manrope_400Regular"; - else if (style.includes("M")) fontFamily = "Manrope_500Medium"; - else if (style.includes("BO") || style.includes("H")) - fontFamily = "Manrope_700Bold"; - else { - const baseStyle = style.slice(0, 6) as TextStyleBase; - switch (baseStyle) { - case "text10": - case "text20": - fontFamily = "Manrope_700Bold"; - break; - case "text30": - case "text40": - fontFamily = "Manrope_600SemiBold"; - break; - case "text50": - fontFamily = "Manrope_400Regular"; - break; - default: - fontFamily = "Manrope_300Light"; - } - } - - switch (style.slice(0, 6) as TextStyleBase) { - case "text10": - fontSize = 64; - break; - case "text20": - fontSize = 50; - break; - case "text30": - fontSize = 36; - break; - case "text40": - fontSize = 28; - break; - case "text50": - fontSize = 24; - break; - case "text60": - fontSize = 20; - break; - case "text70": - fontSize = 16; - break; - case "text80": - fontSize = 14; - break; - case "text90": - fontSize = 12; - break; - case "text100": - fontSize = 10; - break; - default: - fontSize = 16; - } - - return { fontFamily, fontSize }; -}; - -export default function RootLayout() { - const [loaded] = useFonts({ Manrope_200ExtraLight, Manrope_300Light, Manrope_400Regular, @@ -172,20 +8,25 @@ export default function RootLayout() { Manrope_600SemiBold, Manrope_700Bold, Manrope_800ExtraBold, + useFonts, +} from "@expo-google-fonts/manrope"; +import { PlusJakartaSans_200ExtraLight, - PlusJakartaSans_300Light, - PlusJakartaSans_400Regular, - PlusJakartaSans_500Medium, - PlusJakartaSans_600SemiBold, - PlusJakartaSans_700Bold, - PlusJakartaSans_800ExtraBold, PlusJakartaSans_200ExtraLight_Italic, + PlusJakartaSans_300Light, PlusJakartaSans_300Light_Italic, + PlusJakartaSans_400Regular, PlusJakartaSans_400Regular_Italic, + PlusJakartaSans_500Medium, PlusJakartaSans_500Medium_Italic, + PlusJakartaSans_600SemiBold, PlusJakartaSans_600SemiBold_Italic, + PlusJakartaSans_700Bold, PlusJakartaSans_700Bold_Italic, + PlusJakartaSans_800ExtraBold, PlusJakartaSans_800ExtraBold_Italic, +} from "@expo-google-fonts/plus-jakarta-sans"; +import { Poppins_100Thin, Poppins_100Thin_Italic, Poppins_200ExtraLight, @@ -204,72 +45,223 @@ export default function RootLayout() { Poppins_800ExtraBold_Italic, Poppins_900Black, Poppins_900Black_Italic, - }); +} from "@expo-google-fonts/poppins"; +import {Stack} from "expo-router"; +import * as SplashScreen from "expo-splash-screen"; +import "react-native-reanimated"; +import {AuthContextProvider} from "@/contexts/AuthContext"; +import {QueryClient, QueryClientProvider} from "react-query"; +import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib"; - useEffect(() => { - if (loaded) { - SplashScreen.hideAsync(); +SplashScreen.preventAutoHideAsync(); - const typographies: Partial> = {}; - ( - [ - "text10", - "text20", - "text30", - "text40", - "text50", - "text60", - "text70", - "text80", - "text90", - "text100", - ] as const - ).forEach((baseStyle) => { - typographies[baseStyle] = getManropeFontStyle(baseStyle); - (["R", "M", "BO", "H", "BL", "L"] as const).forEach((modifier) => { - const style = `${baseStyle}${modifier}` as TextStyle; - typographies[style] = getManropeFontStyle(style); - }); - }); +const queryClient = new QueryClient(); - Typography.loadTypographies(typographies); - - ThemeManager.setComponentTheme( - "Text", - (props: ExtendedTextProps, context: unknown) => { - const textStyle = ( - Object.keys(props) as Array - ).find((key) => typographies[key as TextStyle]) as - | TextStyle - | undefined; - - return { - style: [ - Typography.text50, - textStyle ? typographies[textStyle] : undefined, - ], - }; - } - ); - } - }, [loaded]); - - if (!loaded) { - return null; - } - - return ( - - - - - - - - - - - - - ); +if (__DEV__) { + // functions().useEmulator("localhost", 5001); + // firestore().useEmulator("localhost", 5471); + // auth().useEmulator("http://localhost:9099"); +} + +type TextStyleBase = + | "text10" + | "text20" + | "text30" + | "text40" + | "text50" + | "text60" + | "text70" + | "text80" + | "text90" + | "text100"; +type TextStyleModifier = "R" | "M" | "BO" | "H" | "BL" | "L"; +type TextStyle = TextStyleBase | `${TextStyleBase}${TextStyleModifier}`; + +type TextStyleProps = { + [K in TextStyle]?: boolean; +}; + +type ExtendedTextProps = TextProps & TextStyleProps; + +interface FontStyle { + fontFamily: string; + fontSize: number; +} + +const getManropeFontStyle = (style: TextStyle): FontStyle => { + let fontFamily: string; + let fontSize: number; + + if (style.includes("L") || style.includes("BL")) + fontFamily = "Manrope_300Light"; + else if (style.includes("R")) fontFamily = "Manrope_400Regular"; + else if (style.includes("M")) fontFamily = "Manrope_500Medium"; + else if (style.includes("BO") || style.includes("H")) + fontFamily = "Manrope_700Bold"; + else { + const baseStyle = style.slice(0, 6) as TextStyleBase; + switch (baseStyle) { + case "text10": + case "text20": + fontFamily = "Manrope_700Bold"; + break; + case "text30": + case "text40": + fontFamily = "Manrope_600SemiBold"; + break; + case "text50": + fontFamily = "Manrope_400Regular"; + break; + default: + fontFamily = "Manrope_300Light"; + } + } + + switch (style.slice(0, 6) as TextStyleBase) { + case "text10": + fontSize = 64; + break; + case "text20": + fontSize = 50; + break; + case "text30": + fontSize = 36; + break; + case "text40": + fontSize = 28; + break; + case "text50": + fontSize = 24; + break; + case "text60": + fontSize = 20; + break; + case "text70": + fontSize = 16; + break; + case "text80": + fontSize = 14; + break; + case "text90": + fontSize = 12; + break; + case "text100": + fontSize = 10; + break; + default: + fontSize = 16; + } + + return {fontFamily, fontSize}; +}; + +export default function RootLayout() { + const [loaded] = useFonts({ + Manrope_200ExtraLight, + Manrope_300Light, + Manrope_400Regular, + Manrope_500Medium, + Manrope_600SemiBold, + Manrope_700Bold, + Manrope_800ExtraBold, + PlusJakartaSans_200ExtraLight, + PlusJakartaSans_300Light, + PlusJakartaSans_400Regular, + PlusJakartaSans_500Medium, + PlusJakartaSans_600SemiBold, + PlusJakartaSans_700Bold, + PlusJakartaSans_800ExtraBold, + PlusJakartaSans_200ExtraLight_Italic, + PlusJakartaSans_300Light_Italic, + PlusJakartaSans_400Regular_Italic, + PlusJakartaSans_500Medium_Italic, + PlusJakartaSans_600SemiBold_Italic, + PlusJakartaSans_700Bold_Italic, + PlusJakartaSans_800ExtraBold_Italic, + Poppins_100Thin, + Poppins_100Thin_Italic, + Poppins_200ExtraLight, + Poppins_200ExtraLight_Italic, + Poppins_300Light, + Poppins_300Light_Italic, + Poppins_400Regular, + Poppins_400Regular_Italic, + Poppins_500Medium, + Poppins_500Medium_Italic, + Poppins_600SemiBold, + Poppins_600SemiBold_Italic, + Poppins_700Bold, + Poppins_700Bold_Italic, + Poppins_800ExtraBold, + Poppins_800ExtraBold_Italic, + Poppins_900Black, + Poppins_900Black_Italic, + }); + + useEffect(() => { + if (loaded) { + SplashScreen.hideAsync(); + + const typographies: Partial> = {}; + ( + [ + "text10", + "text20", + "text30", + "text40", + "text50", + "text60", + "text70", + "text80", + "text90", + "text100", + ] as const + ).forEach((baseStyle) => { + typographies[baseStyle] = getManropeFontStyle(baseStyle); + (["R", "M", "BO", "H", "BL", "L"] as const).forEach((modifier) => { + const style = `${baseStyle}${modifier}` as TextStyle; + typographies[style] = getManropeFontStyle(style); + }); + }); + + Typography.loadTypographies(typographies); + + ThemeManager.setComponentTheme( + "Text", + (props: ExtendedTextProps) => { + const textStyle = ( + Object.keys(props) as Array + ).find((key) => typographies[key as TextStyle]) as + | TextStyle + | undefined; + + return { + style: [ + Typography.text50, + textStyle ? typographies[textStyle] : undefined, + ], + }; + } + ); + } + }, [loaded]); + + if (!loaded) { + return null; + } + + return ( + + + + + + + + + + + + + ); } diff --git a/components/pages/calendar/AddEventDialog.tsx b/components/pages/calendar/AddEventDialog.tsx index 4e92c30..e6afdf4 100644 --- a/components/pages/calendar/AddEventDialog.tsx +++ b/components/pages/calendar/AddEventDialog.tsx @@ -1,188 +1,172 @@ -import React, { useState } from "react"; -import { - AntDesign, - Feather, - MaterialCommunityIcons, - MaterialIcons, -} from "@expo/vector-icons"; -import { - Button, - ButtonSize, - Card, - Dialog, - PanningProvider, - Text, - View, -} from "react-native-ui-lib"; -import { StyleSheet, TouchableOpacity } from "react-native"; -import { ManuallyAddEventModal } from "@/components/pages/calendar/ManuallyAddEventModal"; +import React, {useState} from "react"; +import {MaterialIcons,} from "@expo/vector-icons"; +import {Button, Card, Dialog, PanningProvider, Text, View,} from "react-native-ui-lib"; +import {StyleSheet, TouchableOpacity} from "react-native"; import AddChoreDialog from "../todos/AddChoreDialog"; -import { ToDosContextProvider } from "@/contexts/ToDosContext"; +import {ToDosContextProvider} from "@/contexts/ToDosContext"; import UploadImageDialog from "./UploadImageDialog"; import CameraIcon from "@/assets/svgs/CameraIcon"; import CalendarIcon from "@/assets/svgs/CalendarIcon"; import NavToDosIcon from "@/assets/svgs/NavToDosIcon"; +import {useSetAtom} from "jotai"; +import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms"; export const AddEventDialog = () => { - const [show, setShow] = useState(false); - const [showManualInputModal, setShowManualInputModal] = useState(false); - const [choreDialogVisible, setChoreDialogVisible] = useState(false); - const [showUploadDialog, setShowUploadDialog] = useState(false); + const [show, setShow] = useState(false); + const [choreDialogVisible, setChoreDialogVisible] = useState(false); + const [showUploadDialog, setShowUploadDialog] = useState(false); + const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom) - const handleOpenManualInputModal = () => { - setShow(false); - setTimeout(() => { - setShowManualInputModal(true); - }, 500); - }; + const handleOpenManualInputModal = () => { + setShow(false); + setTimeout(() => { + setSelectedNewEndDate(new Date()); + }, 500); + }; - const handleScanImageDialog = () => { - setShow(false); - setTimeout(() => { - setShowUploadDialog(true); - }, 100); - }; + const handleScanImageDialog = () => { + setShow(false); + setTimeout(() => { + setShowUploadDialog(true); + }, 100); + }; - return ( - - <> - + return ( + + <> + - setShow(false)} - panDirection={PanningProvider.Directions.DOWN} - center - > - - - Create a new event - + setShow(false)} + panDirection={PanningProvider.Directions.DOWN} + center + > + + + Create a new event + - - - - setShowManualInputModal(false)} - /> - - - - ); + setShow(false)}> + + Go back to calendar + + + + + + + + + ); }; const styles = StyleSheet.create({ - modalTitle: { - fontSize: 22, - fontFamily: "Manrope_600SemiBold", - marginBottom: 16, - }, - bottomText: { - marginTop: 20, - color: "#999999", - fontSize: 13.53, - fontFamily: "Poppins_500Medium", - }, - dialogCard: { - paddingHorizontal: 40, - paddingTop: 35, - paddingBottom: 20, - justifyContent: "center", - alignItems: "center", - borderRadius: 20, - }, - btnLabel: { - fontSize: 15, - fontFamily: "PlusJakartaSans_500Medium", - }, - btnIcon: { marginRight: 10 }, + modalTitle: { + fontSize: 22, + fontFamily: "Manrope_600SemiBold", + marginBottom: 16, + }, + bottomText: { + marginTop: 20, + color: "#999999", + fontSize: 13.53, + fontFamily: "Poppins_500Medium", + }, + dialogCard: { + paddingHorizontal: 40, + paddingTop: 35, + paddingBottom: 20, + justifyContent: "center", + alignItems: "center", + borderRadius: 20, + }, + btnLabel: { + fontSize: 15, + fontFamily: "PlusJakartaSans_500Medium", + }, + btnIcon: {marginRight: 10}, }); diff --git a/components/pages/calendar/CalendarHeader.tsx b/components/pages/calendar/CalendarHeader.tsx new file mode 100644 index 0000000..d2d221a --- /dev/null +++ b/components/pages/calendar/CalendarHeader.tsx @@ -0,0 +1,102 @@ +import React, {memo} from 'react'; +import {Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib"; +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"; + + +export const CalendarHeader = memo(() => { + const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom) + const [mode, setMode] = useAtom(modeAtom) + + const handleSegmentChange = (index: number) => { + const selectedMode = modeMap.get(index); + if (selectedMode) { + setMode(selectedMode as "day" | "week" | "month"); + } + }; + + const handleMonthChange = (month: string) => { + const currentDay = selectedDate.getDate(); + const currentYear = selectedDate.getFullYear(); + + const newMonthIndex = months.indexOf(month); + + const updatedDate = new Date(currentYear, newMonthIndex, currentDay); + + setSelectedDate(updatedDate); + }; + + return ( + + + + {selectedDate.getFullYear()} + + handleMonthChange(itemValue as string)} + trailingAccessory={} + topBarProps={{ + title: selectedDate.getFullYear().toString(), + titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17}, + }} + > + {months.map((month) => ( + + ))} + + + + + + + + ); +}); + +const styles = StyleSheet.create({ + segmentslblStyle: { + fontSize: 12, + fontFamily: "Manrope_600SemiBold", + }, + calHeader: { + borderWidth: 0, + }, + dayModeHeader: { + alignSelf: "flex-start", + justifyContent: "space-between", + alignContent: "center", + width: 38, + right: 42, + }, +}); \ No newline at end of file diff --git a/components/pages/calendar/CalendarPage.tsx b/components/pages/calendar/CalendarPage.tsx index 0cfd397..f118eaa 100644 --- a/components/pages/calendar/CalendarPage.tsx +++ b/components/pages/calendar/CalendarPage.tsx @@ -1,207 +1,20 @@ -import React, { useRef, useState } from "react"; -import { LayoutChangeEvent, StyleSheet } from "react-native"; -import { Calendar } from "react-native-big-calendar"; -import { - Picker, - PickerModes, - SegmentedControl, - View, -} from "react-native-ui-lib"; -import { MaterialIcons } from "@expo/vector-icons"; -import { AddEventDialog } from "@/components/pages/calendar/AddEventDialog"; +import React from "react"; +import {View,} from "react-native-ui-lib"; import HeaderTemplate from "@/components/shared/HeaderTemplate"; -import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch"; -import { ManuallyAddEventModal } from "@/components/pages/calendar/ManuallyAddEventModal"; -import { CalendarEvent } from "@/contexts/CalendarContext"; -import { useSettingsContext } from "@/contexts/SettingsContext"; -import EditEventDialog from "./EditEventDialog"; -import { useGetEvents } from "@/hooks/firebase/useGetEvents"; -import { Text } from "react-native-ui-lib"; - -const modeMap = new Map([ - [0, "day"], - [1, "week"], - [2, "month"], -]); - -const months = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", -]; +import {InnerCalendar} from "@/components/pages/calendar/InnerCalendar"; export default function CalendarPage() { - const { calendarColor } = useSettingsContext(); - const [editVisible, setEditVisible] = useState(false); - const [eventForEdit, setEventForEdit] = useState(); - - const styles = StyleSheet.create({ - segmentslblStyle: { - fontSize: 12, - fontFamily: "Manrope_600SemiBold", - }, - calHeader: { - borderWidth: 0, - }, - dayModeHeader: { - alignSelf: "flex-start", - justifyContent: "space-between", - alignContent: "center", - width: 38, - right: 42, - }, - }); - - const [isFamilyView, setIsFamilyView] = useState(false); - const [calendarHeight, setCalendarHeight] = useState(0); - const [mode, setMode] = useState<"week" | "month" | "day">("week"); - const [selectedDate, setSelectedDate] = useState(new Date()); - const [selectedNewEventDate, setSelectedNewEndDate] = useState< - Date | undefined - >(undefined); - - const calendarContainerRef = useRef(null); - const { data: events } = useGetEvents(isFamilyView); - - const onLayout = (event: LayoutChangeEvent) => { - const { height } = event.nativeEvent.layout; - setCalendarHeight(height); - }; - - const handleSegmentChange = (index: number) => { - const selectedMode = modeMap.get(index); - if (selectedMode) { - setMode(selectedMode as "day" | "week" | "month"); - } - }; - - const handleMonthChange = (month: string) => { - const currentDay = selectedDate.getDate(); - const currentYear = selectedDate.getFullYear(); - - const newMonthIndex = months.indexOf(month); - - const updatedDate = new Date(currentYear, newMonthIndex, currentDay); - - setSelectedDate(updatedDate); - }; - - return ( - - - - + return ( - - - {selectedDate.getFullYear()} - - handleMonthChange(itemValue as string)} - trailingAccessory={} - topBarProps={{ - title: selectedDate.getFullYear().toString(), - titleStyle: { fontFamily: "Manrope_500Medium", fontSize: 17 }, - }} - > - {months.map((month) => ( - - ))} - - - - - - + - - {calendarHeight > 0 && ( - ({ backgroundColor: event.eventColor })} - onPressEvent={(event) => { - setEditVisible(true); - setEventForEdit(event); - }} - height={calendarHeight} - activeDate={selectedDate} - date={selectedDate} - onPressCell={setSelectedNewEndDate} - headerContentStyle={mode === "day" ? styles.dayModeHeader : {}} - onSwipeEnd={(date) => { - setSelectedDate(date); - }} - /> - )} - - - - {eventForEdit && ( - { - setEditVisible(!editVisible); - }} - event={eventForEdit} - /> - )} - - setSelectedNewEndDate(undefined)} - /> - - ); + ); } diff --git a/components/pages/calendar/CalendarViewSwitch.tsx b/components/pages/calendar/CalendarViewSwitch.tsx index 2bbc376..ac46a97 100644 --- a/components/pages/calendar/CalendarViewSwitch.tsx +++ b/components/pages/calendar/CalendarViewSwitch.tsx @@ -1,90 +1,90 @@ -import { View, Text, Button, TouchableOpacity } from "react-native-ui-lib"; -import React, { useState } from "react"; -import { MaterialIcons } from "@expo/vector-icons"; -import { StyleSheet } from "react-native"; +import {Text, TouchableOpacity, View} from "react-native-ui-lib"; +import React, {useState} from "react"; +import {StyleSheet} from "react-native"; +import {useSetAtom} from "jotai"; +import {isFamilyViewAtom} from "@/components/pages/calendar/atoms"; -interface ICalendarViewProps { - viewSwitch: (value: boolean) => void; -} -const CalendarViewSwitch = (calendarViewProps: ICalendarViewProps) => { - const [calView, setCalView] = useState(false); - return ( - - { - setCalView(true); - calendarViewProps.viewSwitch(true); - }} - > +const CalendarViewSwitch = () => { + const [calView, setCalView] = useState(false); + const viewSwitch = useSetAtom(isFamilyViewAtom) + + return ( - - Family View - - - + { + setCalView(true); + viewSwitch(true); + }} + > + + + Family View + + + - { - setCalView(false); - calendarViewProps.viewSwitch(false); - }} - > - - - My View - + { + setCalView(false); + viewSwitch(false); + }} + > + + + My View + + + - - - ); + ); }; export default CalendarViewSwitch; const styles = StyleSheet.create({ - switchBtnActive: { - backgroundColor: "#a1a1a1", - borderRadius: 50, - }, - switchBtn: { - backgroundColor: "white", - borderRadius: 50, - }, - switchTxt:{ - fontSize: 16, - fontFamily: 'Manrope_600SemiBold' - } + switchBtnActive: { + backgroundColor: "#a1a1a1", + borderRadius: 50, + }, + switchBtn: { + backgroundColor: "white", + borderRadius: 50, + }, + switchTxt: { + fontSize: 16, + fontFamily: 'Manrope_600SemiBold' + } }); diff --git a/components/pages/calendar/EditEventDialog.tsx b/components/pages/calendar/EditEventDialog.tsx index 25a67a4..56b73b1 100644 --- a/components/pages/calendar/EditEventDialog.tsx +++ b/components/pages/calendar/EditEventDialog.tsx @@ -1,312 +1,303 @@ -import { View, Text, Button, Switch } from "react-native-ui-lib"; -import React, { useEffect, useState } from "react"; -import { Feather, AntDesign, Ionicons } from "@expo/vector-icons"; -import { - Dialog, - TextField, - DateTimePicker, - Picker, - ButtonSize, -} from "react-native-ui-lib"; -import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView"; -import { StyleSheet } from "react-native"; +import {Button, ButtonSize, DateTimePicker, Dialog, Switch, Text, TextField, View} from "react-native-ui-lib"; +import React from "react"; +import {AntDesign, Feather, Ionicons} from "@expo/vector-icons"; +import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView"; +import {StyleSheet} from "react-native"; import DropModalIcon from "@/assets/svgs/DropModalIcon"; -import { CalendarEvent } from "@/contexts/CalendarContext"; import ClockIcon from "@/assets/svgs/ClockIcon"; import LockIcon from "@/assets/svgs/LockIcon"; import MenuIcon from "@/assets/svgs/MenuIcon"; -import { useUpdateEvent } from "@/hooks/firebase/useUpdateEvent"; +import {useUpdateEvent} from "@/hooks/firebase/useUpdateEvent"; +import {editVisibleAtom, eventForEditAtom} from "@/components/pages/calendar/atoms"; +import {useAtom} from "jotai"; -interface IEditEventDialog { - event: CalendarEvent; - isVisible: boolean; - setIsVisible: (value: boolean) => void; -} -const EditEventDialog = (editEventProps: IEditEventDialog) => { - const [event, setEvent] = useState(editEventProps.event); - const { mutateAsync: updateEvent } = useUpdateEvent(); +const EditEventDialog = () => { + const [isVisible, setIsVisible] = useAtom(editVisibleAtom) + const [event, setEvent] = useAtom(eventForEditAtom) - useEffect(() => { - setEvent(editEventProps.event); - }, [editEventProps.isVisible]); + const {mutateAsync: updateEvent} = useUpdateEvent(); - return ( - editEventProps.setIsVisible(false)} - containerStyle={{ - borderRadius: 10, - backgroundColor: "white", - width: "100%", - alignSelf: "stretch", - padding: 0, - paddingTop: 4, - margin: 0, - }} - visible={editEventProps.isVisible} - > - - - ); + { + setEvent((prevEvent) => ({ + ...prevEvent!, + title: text, + })); + }} + placeholderTextColor="#2d2d30" + text60R + marginT-15 + marginL-30 + /> + + + + + + + All day + + + + + setEvent((prev) => ({...prev!, allDay: value})) + } + /> + + + + + + + { + setEvent((prev) => ({...prev!, start: date})); + }} + /> + + { + setEvent((prev) => ({...prev!, start: date})); + }} + maximumDate={event.end} + dateTimeFormatter={(date) => date.toLocaleTimeString("en-us", + { + hour: "numeric", + minute: "numeric" + })} + mode="time" + marginR-30 + /> + + + {!event.allDay && ( + + + + { + setEvent((prev) => ({...prev!, end: date})); + }} + /> + + { + setEvent((prev) => ({...prev!, end: date})); + }} + dateTimeFormatter={(date) => date.toLocaleTimeString("en-us", + { + hour: "numeric", + minute: "numeric" + })} + mode="time" + marginR-30 + /> + + )} + + + + + + + Assignees + + - - - setShowAddUserDialog(false)} - center - marginT-30 - > - Return to user settings - - - - - setShowNewUserInfoDialog(false)} - > - - - - New User Information - - {setShowNewUserInfoDialog(false)}}> - - - - - - - - } - backgroundColor={Colors.grey60} - style={{ borderRadius: 25 }} - center - /> - {}}> - - Upload User Profile Photo - - - - - Member Status - setSelectedStatus(item)} - style={styles.picker} - showSearch - floatingPlaceholder - > - - - - - - - - {selectedStatus === ProfileType.FAMILY_DEVICE - ? "Device Name" - : "First Name"} - - { + setShowQRCodeDialog(res.data.userId); + }, 500); } - value={firstName} - onChangeText={setFirstName} - style={styles.inputField} - onSubmitEditing={() => {lNameRef.current?.focus()}} - blurOnSubmit={false} - /> + } + }; - {selectedStatus !== ProfileType.FAMILY_DEVICE && ( - <> - Last Name - {emailRef.current?.focus()}} - blurOnSubmit={false} - /> - - )} + useEffect(() => { + setFirstName(""); + setLastName(""); + setEmail(""); + }, []) - {selectedStatus !== ProfileType.FAMILY_DEVICE && ( - <> - Email Address (Optional) - - - )} - - - ); + // @ts-ignore + return ( + + + + {!parents.length && !children.length && !caregivers.length && ( + + {isLoading ? "Loading...." : "No user devices added"} + + )} + + {(!!parents.length || !!children.length) && ( + <> + + Family + + {[...parents, ...children]?.map((member, index) => ( + + + + + {member.firstName} {member.lastName} + + + {member.userType === ProfileType.PARENT + ? "Admin (You)" + : "Child"} + + + + + + setShowQRCodeDialog("")} + showQRCodeDialog={showQRCodeDialog === member?.uid} + userId={member?.uid!} + /> + + ))} + + )} + + {!!caregivers.length && ( + <> + + Caregivers + + {caregivers?.map((member) => ( + + + + + {member.firstName} {member.lastName} + + + Caregiver + + + + setShowQRCodeDialog("")} + showQRCodeDialog={showQRCodeDialog === member?.uid} + userId={member?.uid!} + /> + + ))} + + )} + + {!!familyDevices.length && ( + <> + + Family Devices + + {familyDevices?.map((member, index) => ( + + + + {member.firstName} + + Family Device + + + + setShowQRCodeDialog("")} + showQRCodeDialog={showQRCodeDialog === member?.uid} + userId={member?.uid!} + /> + + ))} + + )} + + + + setShowAddUserDialog(true), + style: styles.bottomButton, + }} + /> + + setShowAddUserDialog(false)} + panDirection={PanningProvider.Directions.DOWN} + > + + + Add a new user device + + + + + + setShowAddUserDialog(false)} + center + marginT-30 + > + Return to user settings + + + + + setShowNewUserInfoDialog(false)} + > + + + + New User Information + + { + setShowNewUserInfoDialog(false) + }}> + + + + + + + + } + backgroundColor={Colors.grey60} + style={{borderRadius: 25}} + center + /> + { + }}> + + Upload User Profile Photo + + + + + Member Status + + setSelectedStatus(item)} + showSearch + floatingPlaceholder + style={styles.inViewPicker} + trailingAccessory={ + + + + } + > + + + + + + + + + {selectedStatus === ProfileType.FAMILY_DEVICE + ? "Device Name" + : "First Name"} + + { + lNameRef.current?.focus() + }} + blurOnSubmit={false} + /> + + {selectedStatus !== ProfileType.FAMILY_DEVICE && ( + <> + Last Name + { + emailRef.current?.focus() + }} + blurOnSubmit={false} + /> + + )} + + {selectedStatus !== ProfileType.FAMILY_DEVICE && ( + <> + Email Address (Optional) + + + )} + + + + ); }; const styles = StyleSheet.create({ - dialogBtn: { - height: 47, - width: 279, - }, - dialogTitle: { fontFamily: "Manrope_600SemiBold", fontSize: 22 }, - dialogBackBtn: { - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 15, - color: "#a7a7a7", - }, - card: { - marginVertical: 15, - backgroundColor: "white", - width: "100%", - borderRadius: 15, - padding: 20, - }, - bottomButton: { - position: "absolute", - bottom: 80, - width: "100%", - }, - familyCard: { - marginBottom: 10, - borderRadius: 10, - backgroundColor: Colors.white, - width: "100%", - }, - inputField: { - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 13, - color: "#565656", - borderRadius: 50, - paddingVertical: 12, - paddingHorizontal: 16, - backgroundColor: Colors.grey80, - marginBottom: 16, - borderColor: Colors.grey50, - borderWidth: 1, - height: 40, - }, - picker: { - borderRadius: 50, - paddingVertical: 12, - paddingHorizontal: 16, - backgroundColor: Colors.grey80, - marginBottom: 16, - borderColor: Colors.grey50, - borderWidth: 1, - marginTop: -20, - height: 40, - }, - label: { - marginBottom: 5, - fontSize: 12, - color: Colors.grey40, - }, - dialogCard: { - borderRadius: 10, - gap: 10, - }, - subTit: { - fontFamily: "Manrope_500Medium", - fontSize: 15, - }, - dialogBtnLbl: { - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 15, - color: "white", - }, - divider: { height: 0.7, backgroundColor: "#e6e6e6", width: "100%" }, - jakarta12: { - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 12, - color: "#a1a1a1", - }, - jakarta13: { - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 13, - }, + dialogBtn: { + height: 47, + width: 279, + }, + dialogTitle: {fontFamily: "Manrope_600SemiBold", fontSize: 22}, + dialogBackBtn: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 15, + color: "#a7a7a7", + }, + card: { + marginVertical: 15, + backgroundColor: "white", + width: "100%", + borderRadius: 15, + padding: 20, + }, + bottomButton: { + position: "absolute", + bottom: 80, + width: "100%", + }, + familyCard: { + marginBottom: 10, + borderRadius: 10, + backgroundColor: Colors.white, + width: "100%", + }, + inputField: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 13, + color: "#565656", + borderRadius: 50, + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: Colors.grey80, + marginBottom: 16, + borderColor: Colors.grey50, + borderWidth: 1, + height: 40, + }, + picker: { + borderRadius: 50, + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: Colors.grey80, + marginBottom: 16, + borderColor: Colors.grey50, + borderWidth: 1, + marginTop: -20, + height: 40, + zIndex: 10, + }, + viewPicker: { + borderRadius: 50, + backgroundColor: Colors.grey80, + marginBottom: 16, + borderColor: Colors.grey50, + borderWidth: 1, + marginTop: 0, + height: 40, + zIndex: 10, + }, + inViewPicker: { + borderRadius: 50, + paddingVertical: 12, + paddingHorizontal: 16, + marginBottom: 16, + marginTop: -20, + height: 40, + zIndex: 10, + }, + label: { + marginBottom: 5, + fontSize: 12, + color: Colors.grey40, + }, + dialogCard: { + borderRadius: 10, + gap: 10, + }, + subTit: { + fontFamily: "Manrope_500Medium", + fontSize: 15, + }, + dialogBtnLbl: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 15, + color: "white", + }, + divider: {height: 0.7, backgroundColor: "#e6e6e6", width: "100%"}, + jakarta12: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 12, + color: "#a1a1a1", + }, + jakarta13: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 13, + }, }); export default MyGroup; diff --git a/components/pages/settings/user_settings_views/MyProfile.tsx b/components/pages/settings/user_settings_views/MyProfile.tsx index 609f50f..fe91d08 100644 --- a/components/pages/settings/user_settings_views/MyProfile.tsx +++ b/components/pages/settings/user_settings_views/MyProfile.tsx @@ -1,122 +1,207 @@ -import { Text, TextField, View } from "react-native-ui-lib"; -import React, { useState } from "react"; -import { ImageBackground, StyleSheet } from "react-native"; -import { ScrollView } from "react-native-gesture-handler"; -import { useAuthContext } from "@/contexts/AuthContext"; -import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData"; +import {Colors, Picker, Text, TextField, View} from "react-native-ui-lib"; +import React, {useEffect, useRef, useState} from "react"; +import {ImageBackground, StyleSheet} from "react-native"; +import {ScrollView} from "react-native-gesture-handler"; +import {useAuthContext} from "@/contexts/AuthContext"; +import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData"; +import Ionicons from "@expo/vector-icons/Ionicons"; +import * as tz from 'tzdata'; +import * as Localization from 'expo-localization'; +import debounce from "debounce"; const MyProfile = () => { - const { user, profileData } = useAuthContext(); + const {user, profileData} = useAuthContext(); - const [lastName, setLastName] = useState(profileData?.lastName || ""); - const [firstName, setFirstName] = useState( - profileData?.firstName || "" - ); + const [timeZone, setTimeZone] = useState(profileData?.timeZone! ?? Localization.getCalendars()[0].timeZone); + const [lastName, setLastName] = useState(profileData?.lastName || ""); + const [firstName, setFirstName] = useState( + profileData?.firstName || "" + ); - const { mutateAsync: updateUserData } = useUpdateUserData(); - return ( - - - Your Profile - - + const {mutateAsync: updateUserData} = useUpdateUserData(); + const isFirstRender = useRef(true); - - Change Photo - - Remove Photo - - - - First name - - { - setFirstName(value); - await updateUserData({ newUserData: { firstName: value } }); - }} - /> - - Last name - - { - setLastName(value); - await updateUserData({ newUserData: { lastName: value } }); - }} - /> - - Email address - - - - - - Settings - - Time Zone - - - - - ); + const handleUpdateUserData = async () => { + await updateUserData({newUserData: {firstName, lastName, timeZone}}); + } + + const debouncedUserDataUpdate = debounce(handleUpdateUserData, 500); + + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + debouncedUserDataUpdate(); + }, [timeZone, lastName, firstName]); + + return ( + + + Your Profile + + + + + Change Photo + + Remove Photo + + + + First name + + { + setFirstName(value); + }} + /> + + Last name + + { + setLastName(value); + }} + /> + + Email address + + + + + + + Settings + Time Zone + + { + setTimeZone(item as string) + }} + showSearch + floatingPlaceholder + style={styles.inViewPicker} + trailingAccessory={ + + + + } + > + {timeZoneItems} + + + + + ); }; +const timeZoneItems = Object.keys(tz.zones).sort().map((zone) => ( + +)); + const styles = StyleSheet.create({ - card: { - marginVertical: 15, - backgroundColor: "white", - width: "100%", - borderRadius: 12, - paddingHorizontal: 20, - paddingVertical: 21, - }, - pfp: { - aspectRatio: 1, - width: 65.54, - backgroundColor: "green", - borderRadius: 20, - }, - txtBox: { - backgroundColor: "#fafafa", - borderRadius: 50, - borderWidth: 2, - borderColor: "#cecece", - padding: 15, - height: 45, - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 13 - }, - subTit: { - fontFamily: "Manrope_500Medium", - fontSize: 15, - }, - label: { - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 12, - color: "#a1a1a1" - }, - photoSet:{ - fontFamily: "PlusJakartaSans_500Medium", - fontSize: 13.07 - } + card: { + marginVertical: 15, + backgroundColor: "white", + width: "100%", + borderRadius: 12, + paddingHorizontal: 20, + paddingVertical: 21, + }, + pfp: { + aspectRatio: 1, + width: 65.54, + backgroundColor: "green", + borderRadius: 20, + }, + txtBox: { + backgroundColor: "#fafafa", + borderRadius: 50, + borderWidth: 2, + borderColor: "#cecece", + padding: 15, + height: 45, + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 13 + }, + subTit: { + fontFamily: "Manrope_500Medium", + fontSize: 15, + }, + label: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 12, + color: "#a1a1a1" + }, + photoSet: { + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 13.07 + }, + jakarta12: { + paddingVertical: 10, + fontFamily: "PlusJakartaSans_500Medium", + fontSize: 12, + color: "#a1a1a1", + }, + picker: { + borderRadius: 50, + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: Colors.grey80, + marginBottom: 16, + borderColor: Colors.grey50, + borderWidth: 1, + marginTop: -20, + height: 40, + zIndex: 10, + }, + viewPicker: { + borderRadius: 50, + backgroundColor: Colors.grey80, + marginBottom: 16, + borderColor: Colors.grey50, + borderWidth: 1, + marginTop: 0, + height: 40, + zIndex: 10, + }, + inViewPicker: { + borderRadius: 50, + paddingVertical: 12, + paddingHorizontal: 16, + marginBottom: 16, + marginTop: -20, + height: 40, + zIndex: 10, + }, }); export default MyProfile; diff --git a/hooks/firebase/types/profileTypes.ts b/hooks/firebase/types/profileTypes.ts index a73b9e0..8c6ea4a 100644 --- a/hooks/firebase/types/profileTypes.ts +++ b/hooks/firebase/types/profileTypes.ts @@ -24,6 +24,8 @@ export interface UserProfile { googleMail?: string | null; outlookMail?: string | null; appleMail?: string | null; + timeZone?: string | null; + firstDayOfWeek?: string | null; } export interface ParentProfile extends UserProfile { diff --git a/hooks/firebase/useGetEvents.ts b/hooks/firebase/useGetEvents.ts index 79518c7..74b79b1 100644 --- a/hooks/firebase/useGetEvents.ts +++ b/hooks/firebase/useGetEvents.ts @@ -6,7 +6,7 @@ import {useAtomValue} from "jotai"; import {isFamilyViewAtom} from "@/components/pages/calendar/atoms"; export const useGetEvents = () => { - const { user, profileData } = useAuthContext(); + const {user, profileData} = useAuthContext(); const isFamilyView = useAtomValue(isFamilyViewAtom) return useQuery({ @@ -43,5 +43,7 @@ export const useGetEvents = () => { }; })); }, + staleTime: Infinity, + cacheTime: Infinity }); }; diff --git a/hooks/firebase/useSignUp.ts b/hooks/firebase/useSignUp.ts index c9b217b..2464cd0 100644 --- a/hooks/firebase/useSignUp.ts +++ b/hooks/firebase/useSignUp.ts @@ -3,6 +3,7 @@ import auth from "@react-native-firebase/auth"; import { ProfileType } from "@/contexts/AuthContext"; import { useSetUserData } from "./useSetUserData"; import {uuidv4} from "@firebase/util"; +import * as Localization from "expo-localization"; export const useSignUp = () => { const { mutateAsync: setUserData } = useSetUserData(); @@ -30,6 +31,7 @@ export const useSignUp = () => { firstName: firstName, lastName: lastName, familyId: uuidv4(), + timeZone: Localization.getCalendars()[0].timeZone, }, customUser: res.user, }); diff --git a/hooks/firebase/useUpdateUserData.ts b/hooks/firebase/useUpdateUserData.ts index 66596f3..28b3363 100644 --- a/hooks/firebase/useUpdateUserData.ts +++ b/hooks/firebase/useUpdateUserData.ts @@ -1,11 +1,11 @@ import firestore from "@react-native-firebase/firestore"; -import { FirebaseAuthTypes } from "@react-native-firebase/auth"; -import { useMutation, useQueryClient } from "react-query"; -import { useAuthContext } from "@/contexts/AuthContext"; -import { UserProfile } from "@/hooks/firebase/types/profileTypes"; +import {FirebaseAuthTypes} from "@react-native-firebase/auth"; +import {useMutation, useQueryClient} from "react-query"; +import {useAuthContext} from "@/contexts/AuthContext"; +import {UserProfile} from "@/hooks/firebase/types/profileTypes"; export const useUpdateUserData = () => { - const { user: currentUser, refreshProfileData } = useAuthContext(); + const {user: currentUser, refreshProfileData} = useAuthContext(); const queryClient = useQueryClient(); return useMutation({ @@ -17,7 +17,7 @@ export const useUpdateUserData = () => { newUserData: Partial; customUser?: FirebaseAuthTypes.User; }) => { - console.log("Mutation function called with data:", { newUserData, customUser }); + console.log("Mutation function called with data:", {newUserData, customUser}); const user = currentUser ?? customUser; diff --git a/hooks/useFetchAndSaveAppleEvents.ts b/hooks/useFetchAndSaveAppleEvents.ts new file mode 100644 index 0000000..4493da6 --- /dev/null +++ b/hooks/useFetchAndSaveAppleEvents.ts @@ -0,0 +1,32 @@ +import {useMutation} from "react-query"; +import {useAuthContext} from "@/contexts/AuthContext"; +import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent"; +import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils"; + +export const useFetchAndSaveAppleEvents = () => { + const {profileData} = useAuthContext(); + const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); + + return useMutation({ + mutationKey: ["fetchAndSaveAppleEvents"], + mutationFn: async (token?: string, email?: string) => { + const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); + const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5)); + try { + const response = await fetchiPhoneCalendarEvents( + profileData?.familyId!, + email, + timeMin, + timeMax + ); + + console.log(response); + const items = response ?? []; + await createEventsFromProvider(items); + } catch (error) { + console.error("Error fetching and saving Apple Calendar events: ", error); + throw error; + } + }, + }); +}; \ No newline at end of file diff --git a/hooks/useFetchAndSaveGoogleEvents.ts b/hooks/useFetchAndSaveGoogleEvents.ts new file mode 100644 index 0000000..7243c72 --- /dev/null +++ b/hooks/useFetchAndSaveGoogleEvents.ts @@ -0,0 +1,45 @@ +import {useMutation} from "react-query"; +import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils"; +import {useAuthContext} from "@/contexts/AuthContext"; +import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent"; + +export const useFetchAndSaveGoogleEvents = () => { + const {profileData} = useAuthContext(); + const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); + + return useMutation({ + mutationKey: ["fetchAndSaveGoogleEvents"], + mutationFn: async (token?: string, email?: string) => { + console.log("Fetching Google Calendar events..."); + const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); + const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5)); + + console.log("Token: ", token ?? profileData?.googleToken); + + try { + const response = await fetchGoogleCalendarEvents( + token ?? profileData?.googleToken, + email ?? profileData?.googleMail, + profileData?.familyId, + timeMin.toISOString().slice(0, -5) + "Z", + timeMax.toISOString().slice(0, -5) + "Z" + ); + + console.log("Google Calendar events fetched:", response); + + const items = response?.map((item) => { + if (item.allDay) { + item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0)); + item.endDate = item.startDate; + } + return item; + }) || []; + + await createEventsFromProvider(items); + } catch (error) { + console.error("Error fetching Google Calendar events:", error); + throw error; // Ensure errors are propagated to the mutation + } + }, + }); +}; \ No newline at end of file diff --git a/hooks/useFetchAndSaveOutlookEvents.ts b/hooks/useFetchAndSaveOutlookEvents.ts new file mode 100644 index 0000000..3c53fc5 --- /dev/null +++ b/hooks/useFetchAndSaveOutlookEvents.ts @@ -0,0 +1,36 @@ +import { useMutation } from "react-query"; +import { useAuthContext } from "@/contexts/AuthContext"; +import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent"; +import { fetchMicrosoftCalendarEvents } from "@/calendar-integration/microsoft-calendar-utils"; + +export const useFetchAndSaveOutlookEvents = () => { + const { profileData } = useAuthContext(); + const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider(); + + return useMutation({ + mutationKey: ["fetchAndSaveOutlookEvents"], + mutationFn: async (token?: string, email?: string) => { + const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); + const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3)); + + console.log("Token: ", token ?? profileData?.microsoftToken); + + try { + const response = await fetchMicrosoftCalendarEvents( + token ?? profileData?.microsoftToken, + email ?? profileData?.outlookMail, + profileData?.familyId, + timeMin.toISOString().slice(0, -5) + "Z", + timeMax.toISOString().slice(0, -5) + "Z" + ); + + console.log(response); + const items = response ?? []; + await createEventsFromProvider(items); + } catch (error) { + console.error("Error fetching and saving Outlook events: ", error); + throw error; + } + }, + }); +}; \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8f5a0f0..407bbe1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1190,8 +1190,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoAppleAuthentication (6.4.2): + - ExpoModulesCore - ExpoAsset (10.0.10): - ExpoModulesCore + - ExpoCalendar (13.0.5): + - ExpoModulesCore - ExpoCamera (15.0.16): - ExpoModulesCore - ZXingObjC/OneD @@ -1210,6 +1214,8 @@ PODS: - ExpoModulesCore - ExpoKeepAwake (13.0.2): - ExpoModulesCore + - ExpoLocalization (15.0.3): + - ExpoModulesCore - ExpoModulesCore (1.12.24): - DoubleConversion - glog @@ -2802,7 +2808,9 @@ DEPENDENCIES: - expo-dev-launcher (from `../node_modules/expo-dev-launcher`) - expo-dev-menu (from `../node_modules/expo-dev-menu`) - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`) + - ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`) + - ExpoCalendar (from `../node_modules/expo-calendar/ios`) - ExpoCamera (from `../node_modules/expo-camera/ios`) - ExpoCrypto (from `../node_modules/expo-crypto/ios`) - ExpoDevice (from `../node_modules/expo-device/ios`) @@ -2811,6 +2819,7 @@ DEPENDENCIES: - ExpoHead (from `../node_modules/expo-router/ios`) - ExpoImagePicker (from `../node_modules/expo-image-picker/ios`) - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) + - ExpoLocalization (from `../node_modules/expo-localization/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoSystemUI (from `../node_modules/expo-system-ui/ios`) - ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`) @@ -2956,8 +2965,12 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-dev-menu" expo-dev-menu-interface: :path: "../node_modules/expo-dev-menu-interface/ios" + ExpoAppleAuthentication: + :path: "../node_modules/expo-apple-authentication/ios" ExpoAsset: :path: "../node_modules/expo-asset/ios" + ExpoCalendar: + :path: "../node_modules/expo-calendar/ios" ExpoCamera: :path: "../node_modules/expo-camera/ios" ExpoCrypto: @@ -2974,6 +2987,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-image-picker/ios" ExpoKeepAwake: :path: "../node_modules/expo-keep-awake/ios" + ExpoLocalization: + :path: "../node_modules/expo-localization/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" ExpoSystemUI: @@ -3142,7 +3157,9 @@ SPEC CHECKSUMS: expo-dev-launcher: fe4f2c0a0aa627449eeaec5f9f11e04090f97c40 expo-dev-menu: 5b14897ecce3a8cf9e9cf9109344c2c192a3766a expo-dev-menu-interface: be32c09f1e03833050f0ee290dcc86b3ad0e73e4 + ExpoAppleAuthentication: 265219fa0ba1110872079f55f56686b9737b0065 ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875 + ExpoCalendar: 135beb39ea3795f854a4ea287a49f74c9203ce51 ExpoCamera: 929be541d1c1319fcf32f9f5d9df8b97804346b5 ExpoCrypto: 156078f266bf28f80ecf5e2a9c3a0d6ffce07a1c ExpoDevice: fc94f0e42ecdfd897e7590f2874fc64dfa7e9b1c @@ -3151,6 +3168,7 @@ SPEC CHECKSUMS: ExpoHead: fcb28a68ed4ba28f177394d2dfb8a0a8824cd103 ExpoImagePicker: 12a420923383ae38dccb069847218f27a3b87816 ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08 + ExpoLocalization: f04eeec2e35bed01ab61c72ee1768ec04d093d01 ExpoModulesCore: db3e31e694684f08223d713e89f7648c6d3e04d0 ExpoSystemUI: d4f065a016cae6721b324eb659cdee4d4cf0cb26 ExpoWebBrowser: 7595ccac6938eb65b076385fd23d035db9ecdc8e diff --git a/ios/cally.xcodeproj/project.pbxproj b/ios/cally.xcodeproj/project.pbxproj index aae8a40..c9f5a41 100644 --- a/ios/cally.xcodeproj/project.pbxproj +++ b/ios/cally.xcodeproj/project.pbxproj @@ -178,7 +178,9 @@ LastUpgradeCheck = 1130; TargetAttributes = { 13B07F861A680F5B00A75B9A = { + DevelopmentTeam = MV9C3PHV87; LastSwiftMigration = 1250; + ProvisioningStyle = Automatic; }; }; }; @@ -302,6 +304,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseAuth/FirebaseAuth_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle", @@ -337,6 +340,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseAuth_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle", @@ -421,7 +425,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = cally/cally.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = MV9C3PHV87; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", @@ -441,7 +448,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; - PRODUCT_NAME = CallyFamilyPlanner; + PRODUCT_NAME = "Cally"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -457,7 +464,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = cally/cally.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = MV9C3PHV87; INFOPLIST_FILE = cally/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( @@ -472,7 +482,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; - PRODUCT_NAME = CallyFamilyPlanner; + PRODUCT_NAME = "Cally"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/cally/Info.plist b/ios/cally/Info.plist index 7859764..c246950 100644 --- a/ios/cally/Info.plist +++ b/ios/cally/Info.plist @@ -4,10 +4,12 @@ CADisableMinimumFrameDurationOnPhone + CFBundleAllowMixedLocalizations + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Cally - Family Planner + Cally. CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -55,12 +57,20 @@ NSAllowsLocalNetworking + NSCalendarsFullAccessUsageDescription + The app needs to access your calendar. + NSCalendarsUsageDescription + The app needs to access your calendar. NSCameraUsageDescription Allow $(PRODUCT_NAME) to access your camera NSMicrophoneUsageDescription Allow $(PRODUCT_NAME) to access your microphone NSPhotoLibraryUsageDescription Allow $(PRODUCT_NAME) to access your photos + NSRemindersFullAccessUsageDescription + Allow $(PRODUCT_NAME) to access your reminders + NSRemindersUsageDescription + Allow $(PRODUCT_NAME) to access your reminders NSUserActivityTypes $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route @@ -92,6 +102,12 @@ $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route UILaunchStoryboardName SplashScreen diff --git a/ios/cally/cally.entitlements b/ios/cally/cally.entitlements index 018a6e2..3f2ea61 100644 --- a/ios/cally/cally.entitlements +++ b/ios/cally/cally.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.applesignin + + Default + \ No newline at end of file diff --git a/package.json b/package.json index 17b0b52..a729afe 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,11 @@ "debounce": "^2.1.1", "expo": "~51.0.24", "expo-app-loading": "^2.1.1", + "expo-apple-authentication": "~6.4.2", "expo-auth-session": "^5.5.2", "expo-barcode-scanner": "~13.0.1", "expo-build-properties": "~0.12.4", + "expo-calendar": "~13.0.5", "expo-camera": "~15.0.16", "expo-constants": "~16.0.2", "expo-dev-client": "~4.0.27", @@ -54,6 +56,7 @@ "expo-font": "~12.0.10", "expo-image-picker": "~15.0.7", "expo-linking": "~6.3.1", + "expo-localization": "~15.0.3", "expo-notifications": "~0.28.18", "expo-router": "~3.5.20", "expo-splash-screen": "~0.27.5", @@ -84,7 +87,9 @@ "react-native-toast-message": "^2.2.1", "react-native-ui-lib": "^7.27.0", "react-native-web": "~0.19.10", - "react-query": "^3.39.3" + "react-query": "^3.39.3", + "timezonecomplete": "^5.13.1", + "tzdata": "^1.0.42" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/yarn.lock b/yarn.lock index bbbd3ac..2c6e8c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5001,6 +5001,11 @@ expo-app-loading@^2.1.1: dependencies: expo-splash-screen "~0.17.0" +expo-apple-authentication@~6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/expo-apple-authentication/-/expo-apple-authentication-6.4.2.tgz#1c2ea4fcbd1de5736483dccd370cdd6b8e3de15d" + integrity sha512-X4u1n3Ql1hOpztXHbKNq4I1l4+Ff82gC6RmEeW43Eht7VE6E8PrQBpYKw+JJv8osrCJt7R5O1PZwed6WLN5oig== + expo-application@~5.9.0: version "5.9.1" resolved "https://registry.npmjs.org/expo-application/-/expo-application-5.9.1.tgz" @@ -5042,6 +5047,11 @@ expo-build-properties@~0.12.4: ajv "^8.11.0" semver "^7.6.0" +expo-calendar@~13.0.5: + version "13.0.5" + resolved "https://registry.yarnpkg.com/expo-calendar/-/expo-calendar-13.0.5.tgz#cdc85978cb59d99d6fc9a4fcd2c33d56cc7c8008" + integrity sha512-Wkk7eHvlyhWz2csxU6guYA2HFcLUfYpmlsdMy4a6bneBmFqIZG/ldnLKq/lcQ+BCrfI3fOULt3aNdF6SlZtLlw== + expo-camera@~15.0.16: version "15.0.16" resolved "https://registry.npmjs.org/expo-camera/-/expo-camera-15.0.16.tgz" @@ -5153,6 +5163,13 @@ expo-linking@~6.3.0, expo-linking@~6.3.1: expo-constants "~16.0.0" invariant "^2.2.4" +expo-localization@~15.0.3: + version "15.0.3" + resolved "https://registry.yarnpkg.com/expo-localization/-/expo-localization-15.0.3.tgz#772c89b3ab9c925b7eca6911a11ca33980c2b674" + integrity sha512-IfcmlKuKRlowR9qIzL0e+nGHBeNoF7l2GQaOJstc7HZiPjNJ4J1R4D53ZNf483dt7JSkTRJBihdTadOtOEjRdg== + dependencies: + rtl-detect "^1.0.2" + expo-manifests@~0.14.0: version "0.14.3" resolved "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.14.3.tgz" @@ -9276,6 +9293,11 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" +rtl-detect@^1.0.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6" + integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" @@ -10124,6 +10146,13 @@ through@2: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +timezonecomplete@^5.13.1: + version "5.13.1" + resolved "https://registry.yarnpkg.com/timezonecomplete/-/timezonecomplete-5.13.1.tgz#72c05e82b33013bacc7a38e5d554eafc7914b31f" + integrity sha512-41o3TTExXQ03jQML12Tk64b5TYeEy968Umq5vya+08sFWCcYw5fNqrHubD1vj/JGN1NYhFFBCS09rOL3b7nM2w== + dependencies: + tzdata "1.0.25" + tinycolor2@^1.4.1, tinycolor2@^1.4.2: version "1.6.0" resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz" @@ -10313,6 +10342,16 @@ typical@^2.6.0: resolved "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz" integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg== +tzdata@1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/tzdata/-/tzdata-1.0.25.tgz#e8839033c05761e04ef552242e779777becb13d0" + integrity sha512-yAZ/Tv/tBFIPHJGYrOexxW8Swyjszt7rDhIjnIPSqLaP8mzrr3T7D0w4cxQBtToXnQrlFqkEU0stGC/auz0JcQ== + +tzdata@^1.0.42: + version "1.0.42" + resolved "https://registry.yarnpkg.com/tzdata/-/tzdata-1.0.42.tgz#4f278809b50c50e9c865e44969aa7e746b165638" + integrity sha512-hVA4V8g27yz1YB4Ty4UliwJlWrFOoFrFBYFMd9rKUlRlaF+9Fl3gyzxF/+MQOtCH50pPE+XZ/bYOYkRnBDscVQ== + ua-parser-js@^0.7.33: version "0.7.39" resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.39.tgz" From 478341cce4a518be48a61a3bdbf533914ff8ce85 Mon Sep 17 00:00:00 2001 From: Milan Paunovic Date: Sat, 19 Oct 2024 23:40:08 +0200 Subject: [PATCH 6/9] Switch to day view when clicking on days in week and month view. Refresh tokens every 12 hrs --- components/pages/calendar/EventCalendar.tsx | 7 ++- firebase/functions/index.js | 70 ++++++++++++++++++++- ios/cally/Info.plist | 1 + 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/components/pages/calendar/EventCalendar.tsx b/components/pages/calendar/EventCalendar.tsx index 487b097..ca02e5f 100644 --- a/components/pages/calendar/EventCalendar.tsx +++ b/components/pages/calendar/EventCalendar.tsx @@ -20,7 +20,7 @@ export const EventCalendar: React.FC = memo(({calendarHeight const {data: events} = useGetEvents(); const {profileData} = useAuthContext() const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom) - const mode = useAtomValue(modeAtom) + const [mode, setMode] = useAtom(modeAtom) const setEditVisible = useSetAtom(editVisibleAtom) const setEventForEdit = useSetAtom(eventForEditAtom) const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom) @@ -41,7 +41,10 @@ export const EventCalendar: React.FC = memo(({calendarHeight height={calendarHeight} activeDate={selectedDate} date={selectedDate} - onPressCell={setSelectedNewEndDate} + onPressCell={mode === "day" ? setSelectedNewEndDate: (date) => { + setSelectedDate(date) + setMode("day") + }} headerContentStyle={mode === "day" ? styles.dayModeHeader : {}} onSwipeEnd={(date) => { setSelectedDate(date); diff --git a/firebase/functions/index.js b/firebase/functions/index.js index 4827235..0569dd0 100644 --- a/firebase/functions/index.js +++ b/firebase/functions/index.js @@ -11,7 +11,6 @@ const db = admin.firestore(); let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN}); -// Firestore trigger that listens for new events in the 'Events' collection exports.sendNotificationOnEventCreation = functions.firestore .document('Events/{eventId}') .onCreate(async (snapshot, context) => { @@ -193,6 +192,73 @@ exports.generateCustomToken = onRequest(async (request, response) => { } }); +exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async (context) => { + console.log('Running token refresh job...'); + + const profilesSnapshot = await db.collection('Profiles').get(); + + profilesSnapshot.forEach(async (profileDoc) => { + const profileData = profileDoc.data(); + + if (profileData.googleToken) { + try { + const refreshedGoogleToken = await refreshGoogleToken(profileData.googleToken); + await profileDoc.ref.update({ googleToken: refreshedGoogleToken }); + console.log(`Google token updated for user ${profileDoc.id}`); + } catch (error) { + console.error(`Error refreshing Google token for user ${profileDoc.id}:`, error.message); + } + } + + if (profileData.microsoftToken) { + try { + const refreshedMicrosoftToken = await refreshMicrosoftToken(profileData.microsoftToken); + await profileDoc.ref.update({ microsoftToken: refreshedMicrosoftToken }); + console.log(`Microsoft token updated for user ${profileDoc.id}`); + } catch (error) { + console.error(`Error refreshing Microsoft token for user ${profileDoc.id}:`, error.message); + } + } + + if (profileData.appleToken) { + try { + const refreshedAppleToken = await refreshAppleToken(profileData.appleToken); + await profileDoc.ref.update({ appleToken: refreshedAppleToken }); + console.log(`Apple token updated for user ${profileDoc.id}`); + } catch (error) { + console.error(`Error refreshing Apple token for user ${profileDoc.id}:`, error.message); + } + } + }); + + return null; +}); + +// Function to refresh Google token +async function refreshGoogleToken(token) { + // Assuming you use OAuth2 token refresh flow + const response = await axios.post('https://oauth2.googleapis.com/token', { + grant_type: 'refresh_token', + refresh_token: token, // Add refresh token stored previously + client_id: 'YOUR_GOOGLE_CLIENT_ID', + client_secret: 'YOUR_GOOGLE_CLIENT_SECRET', + }); + + return response.data.access_token; // Return new access token +} + +async function refreshMicrosoftToken(token) { + const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', { + grant_type: 'refresh_token', + refresh_token: token, // Add refresh token stored previously + client_id: 'YOUR_MICROSOFT_CLIENT_ID', + client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET', + scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access', + }); + + return response.data.access_token; // Return new access token +} + async function getPushTokensForEvent() { const usersRef = db.collection('Profiles'); const snapshot = await usersRef.get(); @@ -223,4 +289,4 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) { }); return pushTokens; -} +} \ No newline at end of file diff --git a/ios/cally/Info.plist b/ios/cally/Info.plist index c246950..919ffbc 100644 --- a/ios/cally/Info.plist +++ b/ios/cally/Info.plist @@ -108,6 +108,7 @@ $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route UILaunchStoryboardName SplashScreen From d87d67eae50d61da792559ee1c18e1251df3be4c Mon Sep 17 00:00:00 2001 From: Milan Paunovic Date: Sun, 20 Oct 2024 00:53:09 +0200 Subject: [PATCH 7/9] Added attendees --- .../pages/calendar/ManuallyAddEventModal.tsx | 62 +++++++++---- components/shared/AssigneesDisplay.tsx | 90 +++++++++++++++---- hooks/firebase/types/eventData.ts | 1 + hooks/firebase/types/profileTypes.ts | 1 + hooks/firebase/useGetEvents.ts | 3 +- 5 files changed, 120 insertions(+), 37 deletions(-) diff --git a/components/pages/calendar/ManuallyAddEventModal.tsx b/components/pages/calendar/ManuallyAddEventModal.tsx index 5e2e748..f82188a 100644 --- a/components/pages/calendar/ManuallyAddEventModal.tsx +++ b/components/pages/calendar/ManuallyAddEventModal.tsx @@ -5,6 +5,8 @@ import { DateTimePicker, LoaderScreen, Modal, + Picker, + PickerModes, Switch, Text, TextField, @@ -28,6 +30,7 @@ import CameraIcon from "@/assets/svgs/CameraIcon"; import AssigneesDisplay from "@/components/shared/AssigneesDisplay"; import {useAtom} from "jotai"; import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms"; +import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers"; const daysOfWeek = [ {label: "Monday", value: "monday"}, @@ -70,9 +73,13 @@ export const ManuallyAddEventModal = () => { const [startDate, setStartDate] = useState(initialDate ?? new Date()); const [endDate, setEndDate] = useState(initialDate ?? new Date()); + const [selectedAttendees, setSelectedAttendees] = useState([]); + const [repeatInterval, setRepeatInterval] = useState([]); const {mutateAsync: createEvent, isLoading, isError} = useCreateEvent(); + const {data: members} = useGetFamilyMembers(true) + if (!selectedNewEventDate) return null; @@ -111,6 +118,7 @@ export const ManuallyAddEventModal = () => { startDate: finalStartDate, endDate: finalEndDate, allDay: isAllDay, + attendees: selectedAttendees, }; await createEvent(eventData); @@ -338,27 +346,45 @@ export const ManuallyAddEventModal = () => { > Attendees -