Merge branch 'dev'

# Conflicts:
#	app.json
#	ios/cally/Info.plist
#	yarn.lock
This commit is contained in:
Milan Paunovic
2024-10-20 09:15:40 +02:00
60 changed files with 3856 additions and 3087 deletions

View File

@ -1,10 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
@ -22,7 +24,7 @@
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://u.expo.dev/bdb8c57b-25bb-4d36-b3b8-5b09c5092f52"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

View File

@ -1,5 +1,5 @@
<resources>
<string name="app_name">Cally - Family Planner</string>
<string name="app_name">Cally.</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>

View File

@ -1,4 +1,4 @@
rootProject.name = 'Cally - Family Planner'
rootProject.name = 'Cally.'
dependencyResolutionManagement {
versionCatalogs {

View File

@ -1,6 +1,6 @@
{
"expo": {
"name": "Cally - Family Planner",
"name": "Cally.",
"slug": "cally",
"version": "1.0.0",
"orientation": "portrait",
@ -16,7 +16,8 @@
"supportsTablet": true,
"bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "28"
"buildNumber": "31",
"usesAppleSignIn": true
},
"android": {
"adaptiveIcon": {
@ -63,7 +64,18 @@
"defaultChannel": "default"
}
],
"expo-font"
[
"expo-calendar",
{
"calendarPermission": "The app needs to access your calendar."
}
],
[
"expo-apple-authentication"
],
"expo-font",
"expo-localization",
"./plugins/withPodfile"
],
"experiments": {
"typedRoutes": true

View File

@ -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 (
<SettingsContextProvider>
<CalendarProvider>
<CalendarPage />
</CalendarProvider>
</SettingsContextProvider>
);
return (
<SettingsContextProvider>
<CalendarPage/>
</SettingsContextProvider>
);
}

View File

@ -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,231 @@ 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";
import {Platform} from 'react-native';
import KeyboardManager from 'react-native-keyboard-manager';
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
SplashScreen.preventAutoHideAsync();
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
(
[
"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<keyof ExtendedTextProps>
).find((key) => typographies[key as TextStyle]) as
| TextStyle
| undefined;
return {
style: [
Typography.text50,
textStyle ? typographies[textStyle] : undefined,
],
};
}
);
}
}, [loaded]);
if (!loaded) {
return null;
}
return (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<ThemeProvider value={DefaultTheme}>
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(unauth)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<Toast />
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
);
if (Platform.OS === 'ios') {
KeyboardManager.setEnable(true);
KeyboardManager.setToolbarPreviousNextButtonEnable(true);
}
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<Record<TextStyle, FontStyle>> = {};
(
[
"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<keyof ExtendedTextProps>
).find((key) => typographies[key as TextStyle]) as
| TextStyle
| undefined;
return {
style: [
Typography.text50,
textStyle ? typographies[textStyle] : undefined,
],
};
}
);
}
}, [loaded]);
if (!loaded) {
return null;
}
return (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<ThemeProvider value={DefaultTheme}>
<Stack>
<Stack.Screen name="(auth)" options={{headerShown: false}}/>
<Stack.Screen name="(unauth)" options={{headerShown: false}}/>
<Stack.Screen name="+not-found"/>
</Stack>
<Toast/>
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
);
}

View File

@ -0,0 +1,19 @@
import * as React from "react"
import Svg, { SvgProps, Path } from "react-native-svg"
const ArrowRightIcon = (props: SvgProps) => (
<Svg
width={9}
height={15}
viewBox="0 0 9 15"
fill="none"
{...props}
>
<Path
stroke="#ACACAC"
strokeLinecap="round"
strokeWidth={2}
d="M1.272 1.803 7.16 7.69a.16.16 0 0 1 0 .226l-5.887 5.887"
/>
</Svg>
)
export default ArrowRightIcon

View File

@ -0,0 +1,22 @@
import * as React from "react";
import Svg, { SvgProps, Path } from "react-native-svg";
const CircledXIcon = (props: SvgProps) => (
<Svg
width={props.width || 22}
height={props.height || 21}
viewBox="0 0 22 21"
fill="none"
{...props}
>
<Path
stroke={props.color || "#BBB"}
d="M11 20.5c5.523 0 10-4.477 10-10S16.523.5 11 .5 1 4.977 1 10.5s4.477 10 10 10Z"
/>
<Path
stroke={props.color || "#BBB"}
strokeLinecap="round"
d="m13.75 7.75-5.5 5.5m0-5.5 5.5 5.5"
/>
</Svg>
);
export default CircledXIcon;

17
assets/svgs/EmailIcon.tsx Normal file
View File

@ -0,0 +1,17 @@
import * as React from "react"
import Svg, { SvgProps, Path } from "react-native-svg"
const EmailIcon = (props: SvgProps) => (
<Svg
width={props.width || 20}
height={props.height || 16}
viewBox="0 0 20 16"
fill="none"
{...props}
>
<Path
fill={props.color || "#fff"}
d="M19.948 2.385a2.767 2.767 0 0 0-.76-1.422A2.768 2.768 0 0 0 17.225.15H2.774A2.772 2.772 0 0 0 0 2.925v10.15c0 .389.083.763.23 1.101a2.719 2.719 0 0 0 .774 1.035c.48.398 1.1.638 1.77.638h14.452a2.755 2.755 0 0 0 1.961-.813c.245-.245.447-.537.586-.86v-.002c.147-.338.227-.71.227-1.1V2.925c0-.182-.018-.363-.052-.539ZM1.817 1.967a1.34 1.34 0 0 1 .957-.397h14.452a1.335 1.335 0 0 1 1.079.54l-7.575 6.6a1.11 1.11 0 0 1-1.46 0L1.698 2.107c.034-.048.075-.095.119-.139Zm-.398 11.107V3.575l5.482 4.782-5.479 4.777c-.003-.018-.003-.039-.003-.06Zm15.807 1.355H2.774c-.245 0-.475-.064-.67-.178l5.78-5.037.54.47a2.406 2.406 0 0 0 3.155 0l.54-.47 5.778 5.037a1.338 1.338 0 0 1-.671.178Zm1.355-1.354c0 .02 0 .04-.003.059L13.1 8.36l5.48-4.782v9.497Z"
/>
</Svg>
)
export default EmailIcon

20
assets/svgs/QRIcon.tsx Normal file
View File

@ -0,0 +1,20 @@
import * as React from "react"
import Svg, { SvgProps, Path } from "react-native-svg"
const QRIcon = (props: SvgProps) => (
<Svg
width={props.width || 19}
height={props.height || 20}
viewBox="0 0 19 20"
fill="none"
{...props}
>
<Path
stroke={props.color || "#fff"}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.764}
d="M5.086 10.441h4.41v4.41m-7.93-4.41h-.008m4.418 4.41h-.008m3.536 3.528h-.008m7.946-7.938h-.008m-15.876 4.41H2.88m9.702-4.41h1.764M1.557 18.38h4.41M9.497 1.621v5.292m4.939 11.466h1.587c.494 0 .741 0 .93-.096.166-.085.3-.22.385-.386.097-.188.097-.435.097-.93V15.38c0-.494 0-.74-.097-.93a.882.882 0 0 0-.385-.385c-.189-.096-.436-.096-.93-.096h-1.587c-.494 0-.741 0-.93.096a.881.881 0 0 0-.385.386c-.096.188-.096.435-.096.93v1.587c0 .494 0 .74.096.93.084.166.22.3.385.385.189.096.436.096.93.096Zm0-11.466h1.587c.494 0 .741 0 .93-.096.166-.085.3-.22.385-.385.097-.19.097-.436.097-.93V3.914c0-.494 0-.74-.097-.93a.882.882 0 0 0-.385-.385c-.189-.096-.436-.096-.93-.096h-1.587c-.494 0-.741 0-.93.096a.881.881 0 0 0-.385.386c-.096.188-.096.435-.096.93v1.587c0 .494 0 .74.096.93.084.165.22.3.385.385.189.096.436.096.93.096Zm-11.466 0h1.587c.494 0 .741 0 .93-.096.166-.085.3-.22.385-.385.097-.19.097-.436.097-.93V3.914c0-.494 0-.74-.097-.93a.882.882 0 0 0-.385-.385c-.189-.096-.436-.096-.93-.096H2.97c-.494 0-.741 0-.93.096a.882.882 0 0 0-.385.386c-.096.188-.096.435-.096.93v1.587c0 .494 0 .74.096.93.084.165.22.3.385.385.189.096.436.096.93.096Z"
/>
</Svg>
)
export default QRIcon

View File

@ -0,0 +1,41 @@
import * as Calendar from 'expo-calendar';
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
try {
const {status} = await Calendar.requestCalendarPermissionsAsync();
if (status !== 'granted') {
throw new Error("Calendar permission not granted");
}
const defaultCalendarSource = await Calendar.getDefaultCalendarAsync();
if (!defaultCalendarSource) {
throw new Error("No calendar found");
}
const events = await Calendar.getEventsAsync(
[defaultCalendarSource.id],
startDate,
endDate
);
return events.map((event) => {
let isAllDay = event.allDay || false;
const startDateTime = new Date(event.startDate);
const endDateTime = new Date(event.endDate);
return {
id: event.id,
title: event.title,
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email
};
});
} catch (error) {
console.error("Error fetching iPhone Calendar events: ", error);
throw error;
}
}

View File

@ -1,4 +1,4 @@
export async function fetchGoogleCalendarEvents(token, startDate, endDate) {
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
console.log(token);
const response = await fetch(
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`,
@ -45,6 +45,8 @@ export async function fetchGoogleCalendarEvents(token, startDate, endDate) {
startDate: startDateTime,
endDate: endDateTime,
allDay: isAllDay,
familyId,
email
};
googleEvents.push(googleEvent);
});

View File

@ -1,4 +1,4 @@
export async function fetchMicrosoftCalendarEvents(token, startDate, endDate) {
export async function fetchMicrosoftCalendarEvents(token, email, familyId, startDate, endDate) {
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/calendar/calendarView?startDateTime=${startDate}&endDateTime=${endDate}`,
{
@ -34,6 +34,8 @@ export async function fetchMicrosoftCalendarEvents(token, startDate, endDate) {
startDate: startDateTime,
endDate: endDateTime,
allDay: item.isAllDay,
familyId,
email
};

View File

@ -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<string>("");
const [dumpDesc, setDumpDesc] = useState<string>("");
const { width, height } = Dimensions.get("screen");
const { width } = Dimensions.get("screen");
// Refs for the two TextFields
const descriptionRef = useRef<TextFieldRef>(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}
>
<View row spread style={styles.topBtns} marginB-20>
@ -53,16 +56,15 @@ const AddBrainDump = ({
addBrainDumpProps.setIsVisible(false);
}}
/>
<DropModalIcon
style={{ marginTop: 15 }}
onPress={() => addBrainDumpProps.setIsVisible(false)}
/>
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
<DropModalIcon style={{ marginTop: 15 }} />
</TouchableOpacity>
<Button
color="#05a8b6"
label="Save"
style={styles.topBtn}
onPress={() => {
addBrainDump({ id: 99, title: dumpTitle, description: dumpDesc });
addBrainDump({ id: 99, title: dumpTitle.trimEnd().trimStart(), description: dumpDesc.trimEnd().trimStart() });
addBrainDumpProps.setIsVisible(false);
}}
/>
@ -70,20 +72,37 @@ const AddBrainDump = ({
<View marginH-20>
<TextField
value={dumpTitle}
autoFocus
placeholder="Set Title"
text60R
onChangeText={(text) => {
setDumpTitle(text);
}}
onSubmitEditing={() => {
// Move focus to the description field
descriptionRef.current?.focus();
}}
style={styles.title}
blurOnSubmit={false} // Keep the keyboard open when moving focus
returnKeyType="next"
/>
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20 />
<TextField
ref={descriptionRef}
value={dumpDesc}
placeholder="Write Description"
text70
onChangeText={(text) => {
setDumpDesc(text);
}}
style={styles.description}
multiline
numberOfLines={4}
maxLength={255}
onEndEditing={() => {
descriptionRef.current?.blur();
}}
returnKeyType="done"
/>
</View>
</Dialog>
@ -91,11 +110,28 @@ const AddBrainDump = ({
};
const styles = StyleSheet.create({
dialogContainer: {
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: "white",
padding: 0,
paddingTop: 3,
margin: 0,
},
topBtns: {},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
title: {
fontSize: 22,
fontFamily: "Manrope_500Medium",
},
description: {
fontFamily: "Manrope_400Regular",
fontSize: 14,
textAlignVertical: 'top'
},
});
export default AddBrainDump;

View File

@ -98,19 +98,18 @@ const MoveBrainDump = (props: {
<TextField
textAlignVertical="top"
multiline
autoFocus
fieldStyle={{
width: "94%",
}}
style={{
fontFamily: "Manrope_400Regular",
fontSize: 14,
}}
style={styles.description}
placeholder="Add description"
numberOfLines={3}
value={description}
onChangeText={(value) => {
setDescription(value);
}}
returnKeyType="default"
/>
</View>
<View style={styles.divider} />
@ -192,6 +191,10 @@ const styles = StyleSheet.create({
fontSize: 22,
fontFamily: "Manrope_500Medium",
},
description:{
fontFamily: "Manrope_400Regular",
fontSize: 14,
}
});
export default MoveBrainDump;

View File

@ -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<boolean>(false);
const [showUploadDialog, setShowUploadDialog] = useState<boolean>(false);
const [show, setShow] = useState(false);
const [choreDialogVisible, setChoreDialogVisible] = useState<boolean>(false);
const [showUploadDialog, setShowUploadDialog] = useState<boolean>(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 (
<ToDosContextProvider>
<>
<Button
style={{
position: "absolute",
bottom: 20,
right: 20,
height: 40,
borderRadius: 30,
backgroundColor: "#fd1775",
alignItems: "center",
justifyContent: "center",
}}
color="white"
enableShadow
onPress={() => setShow(true)}
>
<View row centerV centerH>
<MaterialIcons name="add" size={22} color={"white"} />
<Text white style={{ fontSize: 16, fontFamily: 'Manrope_600SemiBold' }}>
New
</Text>
</View>
</Button>
return (
<ToDosContextProvider>
<>
<Button
style={{
position: "absolute",
bottom: 20,
right: 20,
height: 40,
borderRadius: 30,
backgroundColor: "#fd1775",
alignItems: "center",
justifyContent: "center",
}}
color="white"
enableShadow
onPress={() => setShow(true)}
>
<View row centerV centerH>
<MaterialIcons name="add" size={22} color={"white"}/>
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold'}}>
New
</Text>
</View>
</Button>
<Dialog
visible={show}
onDismiss={() => setShow(false)}
panDirection={PanningProvider.Directions.DOWN}
center
>
<Card style={styles.dialogCard}>
<Text text60 style={styles.modalTitle}>
Create a new event
</Text>
<Dialog
visible={show}
onDismiss={() => setShow(false)}
panDirection={PanningProvider.Directions.DOWN}
center
>
<Card style={styles.dialogCard}>
<Text text60 style={styles.modalTitle}>
Create a new event
</Text>
<View
style={{ marginTop: 20, alignItems: "center", width: "100%" }}
>
<Button
style={{
marginBottom: 10,
backgroundColor: "#ea156c",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Scan Image"
labelStyle={styles.btnLabel}
onPress={handleScanImageDialog}
iconSource={() => (
<CameraIcon color="white" style={styles.btnIcon} />
)}
/>
<View
style={{marginTop: 20, alignItems: "center", width: "100%"}}
>
<Button
style={{
marginBottom: 10,
backgroundColor: "#ea156c",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Scan Image"
labelStyle={styles.btnLabel}
onPress={handleScanImageDialog}
iconSource={() => (
<CameraIcon color="white" style={styles.btnIcon}/>
)}
/>
<Button
style={{
marginBottom: 10,
backgroundColor: "#e28800",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Create Event"
labelStyle={styles.btnLabel}
onPress={handleOpenManualInputModal}
iconSource={() => (
<CalendarIcon color={"white"} style={styles.btnIcon} />
)}
/>
<Button
style={{
marginBottom: 10,
backgroundColor: "#e28800",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Create Event"
labelStyle={styles.btnLabel}
onPress={handleOpenManualInputModal}
iconSource={() => (
<CalendarIcon color={"white"} style={styles.btnIcon}/>
)}
/>
<Button
style={{
marginBottom: 10,
backgroundColor: "#05a8b6",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Add To Do"
labelStyle={styles.btnLabel}
onPress={() => setChoreDialogVisible(true)}
iconSource={() => (
<NavToDosIcon
color={"white"}
width={23}
style={styles.btnIcon}
/>
)}
/>
</View>
<Button
style={{
marginBottom: 10,
backgroundColor: "#05a8b6",
justifyContent: "center",
width: "100%",
paddingVertical: 13,
}}
label="Add To Do"
labelStyle={styles.btnLabel}
onPress={() => setChoreDialogVisible(true)}
iconSource={() => (
<NavToDosIcon
color={"white"}
width={23}
style={styles.btnIcon}
/>
)}
/>
</View>
<TouchableOpacity onPress={() => setShow(false)}>
<Text style={styles.bottomText} text70>
Go back to calendar
</Text>
</TouchableOpacity>
</Card>
</Dialog>
<AddChoreDialog
isVisible={choreDialogVisible}
setIsVisible={setChoreDialogVisible}
/>
<ManuallyAddEventModal
show={showManualInputModal}
close={() => setShowManualInputModal(false)}
/>
<UploadImageDialog
show={showUploadDialog}
setShow={setShowUploadDialog}
/>
</>
</ToDosContextProvider>
);
<TouchableOpacity onPress={() => setShow(false)}>
<Text style={styles.bottomText} text70>
Go back to calendar
</Text>
</TouchableOpacity>
</Card>
</Dialog>
<AddChoreDialog
isVisible={choreDialogVisible}
setIsVisible={setChoreDialogVisible}
/>
<UploadImageDialog
show={showUploadDialog}
setShow={setShowUploadDialog}
/>
</>
</ToDosContextProvider>
);
};
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},
});

View File

@ -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 (
<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}}
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>
<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>
);
});
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,
},
});

View File

@ -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<boolean>(false);
const [eventForEdit, setEventForEdit] = useState<CalendarEvent>();
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<boolean>(false);
const [calendarHeight, setCalendarHeight] = useState(0);
const [mode, setMode] = useState<"week" | "month" | "day">("week");
const [selectedDate, setSelectedDate] = useState<Date>(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 (
<View
style={{ flex: 1, height: "100%", padding: 10 }}
paddingH-22
paddingT-0
>
<HeaderTemplate
message={"Let's get your week started!"}
isWelcome={true}
/>
<View
style={{ flex: 1, backgroundColor: "#fff", borderRadius: 30 }}
ref={calendarContainerRef}
onLayout={onLayout}
>
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "white",
marginBottom: 10,
}}
style={{flex: 1, height: "100%", padding: 10}}
paddingH-22
paddingT-0
>
<View row centerV gap-3>
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}>
{selectedDate.getFullYear()}
</Text>
<Picker
value={months[selectedDate.getMonth()]} // Get the month from the date
placeholder={"Select Month"}
style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}
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>
<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}
<HeaderTemplate
message={"Let's get your week started!"}
isWelcome
/>
</View>
<InnerCalendar/>
</View>
{calendarHeight > 0 && (
<Calendar
bodyContainerStyle={styles.calHeader}
mode={mode}
events={isFamilyView ? events ?? [] : events ?? []}
eventCellStyle={(event) => ({ 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);
}}
/>
)}
</View>
<CalendarViewSwitch viewSwitch={setIsFamilyView} />
<AddEventDialog />
{eventForEdit && (
<EditEventDialog
isVisible={editVisible}
setIsVisible={() => {
setEditVisible(!editVisible);
}}
event={eventForEdit}
/>
)}
<ManuallyAddEventModal
key={`${selectedNewEventDate}`}
initialDate={selectedNewEventDate}
show={!!selectedNewEventDate}
close={() => setSelectedNewEndDate(undefined)}
/>
</View>
);
);
}

View File

@ -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<boolean>(false);
return (
<View
row
spread
style={{
position: "absolute",
bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
// iOS shadow
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Android shadow (elevation)
elevation: 6,
}}
centerV
>
<TouchableOpacity
onPress={() => {
setCalView(true);
calendarViewProps.viewSwitch(true);
}}
>
const CalendarViewSwitch = () => {
const [calView, setCalView] = useState<boolean>(false);
const viewSwitch = useSetAtom(isFamilyViewAtom)
return (
<View
centerV
centerH
height={40}
paddingH-15
style={calView ? styles.switchBtnActive : styles.switchBtn}
row
spread
style={{
position: "absolute",
bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
// iOS shadow
shadowColor: "#000",
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Android shadow (elevation)
elevation: 6,
}}
centerV
>
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
Family View
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setCalView(true);
viewSwitch(true);
}}
>
<View
centerV
centerH
height={40}
paddingH-15
style={calView ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
Family View
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setCalView(false);
calendarViewProps.viewSwitch(false);
}}
>
<View
centerV
centerH
height={40}
paddingH-15
style={!calView ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
My View
</Text>
<TouchableOpacity
onPress={() => {
setCalView(false);
viewSwitch(false);
}}
>
<View
centerV
centerH
height={40}
paddingH-15
style={!calView ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
My View
</Text>
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
</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'
}
});

View File

@ -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<CalendarEvent>(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 (
<Dialog
bottom={true}
height={"90%"}
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => editEventProps.setIsVisible(false)}
containerStyle={{
borderRadius: 10,
backgroundColor: "white",
width: "100%",
alignSelf: "stretch",
padding: 0,
paddingTop: 4,
margin: 0,
}}
visible={editEventProps.isVisible}
>
<View row spread>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Cancel"
onPress={() => {
editEventProps.setIsVisible(false);
}}
/>
<View marginT-12>
<DropModalIcon
onPress={() => {
editEventProps.setIsVisible(false);
if (!event) return null
return (
<Dialog
bottom={true}
height={"90%"}
panDirection={PanningDirectionsEnum.DOWN}
onDismiss={() => setIsVisible(false)}
containerStyle={{
borderRadius: 10,
backgroundColor: "white",
width: "100%",
alignSelf: "stretch",
padding: 0,
paddingTop: 4,
margin: 0,
}}
/>
</View>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Save"
onPress={() => {
try {
if (event.id) {
updateEvent(event).then(() => editEventProps.setIsVisible(false));
}
} catch (error) {
console.error(error);
}
}}
/>
</View>
<TextField
placeholder="Edit event title"
value={event.title}
onChangeText={(text) => {
setEvent((prevEvent) => ({
...prevEvent,
title: text,
}));
}}
placeholderTextColor="#2d2d30"
text60R
marginT-15
marginL-30
/>
<View style={styles.divider} marginT-8 />
<View row spread marginB-10 marginL-30 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191" />
<Text text70 marginL-10>
All day
</Text>
</View>
<View right marginR-30>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={event.allDay}
onValueChange={(value) =>
setEvent((prev) => ({ ...prev, allDay: value }))
}
/>
</View>
</View>
<View marginL-30 centerV>
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={event.start}
text70
marginL-8
maximumDate={event.end}
onChange={(date) => {
setEvent((prev) => ({ ...prev, start: date }));
}}
/>
</View>
<DateTimePicker
text70
value={event.start}
onChange={(date) => {
setEvent((prev) => ({ ...prev, start: date }));
}}
maximumDate={event.end}
dateTimeFormatter={(date, mode) => date.toLocaleTimeString("en-us",
{ hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>
</View>
{!event.allDay && (
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={event.end}
minimumDate={event.start}
text70
marginL-8
onChange={(date) => {
setEvent((prev) => ({ ...prev, end: date }));
}}
/>
visible={isVisible}
>
<View row spread>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Cancel"
onPress={() => {
setIsVisible(false);
}}
/>
<View marginT-12>
<DropModalIcon
onPress={() => {
setIsVisible(false);
}}
/>
</View>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Save"
onPress={() => {
try {
if (event.id) {
updateEvent(event).then(() => setIsVisible(false));
}
} catch (error) {
console.error(error);
}
}}
/>
</View>
<DateTimePicker
text70
value={event.end}
minimumDate={event.start}
onChange={(date) => {
setEvent((prev) => ({ ...prev, end: date }));
}}
dateTimeFormatter={(date, mode) => date.toLocaleTimeString("en-us",
{ hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>
</View>
)}
</View>
<View style={styles.divider} />
<View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191" />
<Text text70R marginL-10>
Assignees
</Text>
<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"
/>
</View>
<View row marginH-13 marginT-13>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon />
<Text text70 marginL-10>
Reminder
</Text>
</View>
<View>
<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="Set Reminder"
/>
</View>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon />
<Text text70 marginL-10>
Mark as Private
</Text>
</View>
<View>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={event.private}
onValueChange={(value) =>
setEvent((prev) => ({ ...prev, private: value }))
}
/>
</View>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon />
<Text text70 marginL-10>
Add Details
</Text>
</View>
<View></View>
</View>
</Dialog>
);
<TextField
placeholder="Edit event title"
value={event.title}
onChangeText={(text) => {
setEvent((prevEvent) => ({
...prevEvent!,
title: text,
}));
}}
placeholderTextColor="#2d2d30"
text60R
marginT-15
marginL-30
/>
<View style={styles.divider} marginT-8/>
<View row spread marginB-10 marginL-30 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191"/>
<Text text70 marginL-10>
All day
</Text>
</View>
<View right marginR-30>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={event.allDay}
onValueChange={(value) =>
setEvent((prev) => ({...prev!, allDay: value}))
}
/>
</View>
</View>
<View marginL-30 centerV>
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191"/>
<DateTimePicker
value={event.start}
text70
marginL-8
maximumDate={event.end}
onChange={(date) => {
setEvent((prev) => ({...prev!, start: date}));
}}
/>
</View>
<DateTimePicker
text70
value={event.start}
onChange={(date) => {
setEvent((prev) => ({...prev!, start: date}));
}}
maximumDate={event.end}
dateTimeFormatter={(date) => date.toLocaleTimeString("en-us",
{
hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>
</View>
{!event.allDay && (
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191"/>
<DateTimePicker
value={event.end}
minimumDate={event.start}
text70
marginL-8
onChange={(date) => {
setEvent((prev) => ({...prev!, end: date}));
}}
/>
</View>
<DateTimePicker
text70
value={event.end}
minimumDate={event.start}
onChange={(date) => {
setEvent((prev) => ({...prev!, end: date}));
}}
dateTimeFormatter={(date) => date.toLocaleTimeString("en-us",
{
hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>
</View>
)}
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191"/>
<Text text70R marginL-10>
Assignees
</Text>
<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"
/>
</View>
<View row marginH-13 marginT-13>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon/>
<Text text70 marginL-10>
Reminder
</Text>
</View>
<View>
<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="Set Reminder"
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon/>
<Text text70 marginL-10>
Mark as Private
</Text>
</View>
<View>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={event.private}
onValueChange={(value) =>
setEvent((prev) => ({...prev!, private: value}))
}
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon/>
<Text text70 marginL-10>
Add Details
</Text>
</View>
<View></View>
</View>
</Dialog>
);
};
export default EditEventDialog;
const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 },
gradient: {
height: "25%",
position: "absolute",
bottom: 0,
width: "100%",
},
buttonContainer: {
position: "absolute",
bottom: 25,
width: "100%",
},
button: {
backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20,
},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
gradient: {
height: "25%",
position: "absolute",
bottom: 0,
width: "100%",
},
buttonContainer: {
position: "absolute",
bottom: 25,
width: "100%",
},
button: {
backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20,
},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
});

View File

@ -0,0 +1,71 @@
import React, {memo} from 'react';
import {Calendar} from "react-native-big-calendar";
import {StyleSheet} from "react-native";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useAtomValue, useSetAtom} from "jotai";
import {
editVisibleAtom,
eventForEditAtom,
modeAtom,
selectedDateAtom,
selectedNewEventDateAtom
} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
interface EventCalendarProps {
calendarHeight: number;
}
export const EventCalendar: React.FC<EventCalendarProps> = memo(({calendarHeight}) => {
const {data: events} = useGetEvents();
const {profileData} = useAuthContext()
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom)
const [mode, setMode] = useAtom(modeAtom)
const setEditVisible = useSetAtom(editVisibleAtom)
const setEventForEdit = useSetAtom(eventForEditAtom)
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom)
console.log("Events: ", events)
return (
<Calendar
bodyContainerStyle={styles.calHeader}
mode={mode}
events={events ?? []}
eventCellStyle={(event) => ({backgroundColor: event.eventColor})}
onPressEvent={(event) => {
setEditVisible(true);
setEventForEdit(event);
}}
weekStartsOn={profileData?.firstDayOfWeek === "Mondays" ? 1 : 0}
height={calendarHeight}
activeDate={selectedDate}
date={selectedDate}
onPressCell={mode === "day" ? setSelectedNewEndDate: (date) => {
setSelectedDate(date)
setMode("day")
}}
headerContentStyle={mode === "day" ? styles.dayModeHeader : {}}
onSwipeEnd={(date) => {
setSelectedDate(date);
}}
/>
);
});
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,
},
});

View File

@ -0,0 +1,41 @@
import {View} from "react-native-ui-lib";
import React, {useRef, useState} from "react";
import {LayoutChangeEvent} from "react-native";
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
import EditEventDialog from "@/components/pages/calendar/EditEventDialog";
import {ManuallyAddEventModal} from "@/components/pages/calendar/ManuallyAddEventModal";
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
import {EventCalendar} from "@/components/pages/calendar/EventCalendar";
export const InnerCalendar = () => {
const [calendarHeight, setCalendarHeight] = useState(0);
const calendarContainerRef = useRef(null);
const onLayout = (event: LayoutChangeEvent) => {
const {height} = event.nativeEvent.layout;
setCalendarHeight(height);
};
return (
<>
<View
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30, marginBottom: 60}}
ref={calendarContainerRef}
onLayout={onLayout}
>
<CalendarHeader/>
{calendarHeight > 0 && (
<EventCalendar
calendarHeight={calendarHeight}
/>
)}
</View>
<CalendarViewSwitch/>
<AddEventDialog/>
<EditEventDialog/>
<ManuallyAddEventModal/>
</>
)
}

View File

@ -1,488 +1,508 @@
import {
Avatar,
Button,
ButtonSize,
Colors,
DateTimePicker,
LoaderScreen,
Modal,
Picker,
Switch,
Text,
TextField,
TouchableOpacity,
View,
Button,
ButtonSize,
Colors,
DateTimePicker,
LoaderScreen,
Modal,
Picker,
PickerModes,
Switch,
Text,
TextField,
TouchableOpacity,
View,
} from "react-native-ui-lib";
import { ScrollView } from "react-native-gesture-handler";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useState } from "react";
import {
AntDesign,
Feather,
Ionicons,
MaterialIcons,
} from "@expo/vector-icons";
import { PickerMultiValue } from "react-native-ui-lib/src/components/picker/types";
import { useAuthContext } from "@/contexts/AuthContext";
import { useCreateEvent } from "@/hooks/firebase/useCreateEvent";
import { EventData } from "@/hooks/firebase/types/eventData";
import { addHours, setDate } from "date-fns";
import {ScrollView} from "react-native-gesture-handler";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import {useState} from "react";
import {AntDesign, Feather, Ionicons,} from "@expo/vector-icons";
import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateEvent} from "@/hooks/firebase/useCreateEvent";
import {EventData} from "@/hooks/firebase/types/eventData";
import {addHours} from "date-fns";
import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { CalendarEvent, useCalendarContext } from "@/contexts/CalendarContext";
import { repeatOptions } from "@/contexts/ToDosContext";
import { ImageBackground, StyleSheet } from "react-native";
import {StyleSheet} from "react-native";
import ClockIcon from "@/assets/svgs/ClockIcon";
import LockIcon from "@/assets/svgs/LockIcon";
import MenuIcon from "@/assets/svgs/MenuIcon";
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" },
{ label: "Tuesday", value: "tuesday" },
{ label: "Wednesday", value: "wednesday" },
{ label: "Thursday", value: "thursday" },
{ label: "Friday", value: "friday" },
{ label: "Saturday", value: "saturday" },
{ label: "Sunday", value: "sunday" },
{label: "Monday", value: "monday"},
{label: "Tuesday", value: "tuesday"},
{label: "Wednesday", value: "wednesday"},
{label: "Thursday", value: "thursday"},
{label: "Friday", value: "friday"},
{label: "Saturday", value: "saturday"},
{label: "Sunday", value: "sunday"},
];
export const ManuallyAddEventModal = ({
show,
close,
initialDate,
}: {
show: boolean;
close: () => void;
initialDate?: Date;
}) => {
const { addEvent } = useCalendarContext();
const { user } = useAuthContext();
const insets = useSafeAreaInsets();
export const ManuallyAddEventModal = () => {
const insets = useSafeAreaInsets();
const [title, setTitle] = useState<string>("");
const [selectedNewEventDate, setSelectedNewEndDate] = useAtom(selectedNewEventDateAtom)
const [isAllDay, setIsAllDay] = useState(false);
const [isPrivate, setIsPrivate] = useState<boolean>(false);
const [startTime, setStartTime] = useState(() => {
const date = initialDate ?? new Date();
date.setSeconds(0, 0);
return date;
});
const [endTime, setEndTime] = useState(() => {
const date = initialDate
? addHours(initialDate, 1)
: addHours(new Date(), 1);
date.setSeconds(0, 0);
return date;
});
const [startDate, setStartDate] = useState(initialDate ?? new Date());
const [endDate, setEndDate] = useState(initialDate ?? new Date());
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
const { mutateAsync: createEvent, isLoading, isError } = useCreateEvent();
const formatDateTime = (date?: Date | string) => {
if (!date) return undefined;
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
};
const combineDateAndTime = (date: Date, time: Date): Date => {
const combined = new Date(date);
combined.setHours(time.getHours());
combined.setMinutes(time.getMinutes());
combined.setSeconds(0);
combined.setMilliseconds(0);
return combined;
};
const handleSave = async () => {
let finalStartDate: Date;
let finalEndDate: Date;
if (isAllDay) {
finalStartDate = new Date(startDate.setHours(0, 0, 0, 0));
finalEndDate = new Date(startDate.setHours(0, 0, 0, 0));
} else {
finalStartDate = combineDateAndTime(startDate, startTime);
finalEndDate = combineDateAndTime(endDate, endTime);
const {show, close, initialDate} = {
show: !!selectedNewEventDate,
close: () => setSelectedNewEndDate(undefined),
initialDate: selectedNewEventDate
}
const eventData: Partial<EventData> = {
title: title,
startDate: finalStartDate,
endDate: finalEndDate,
allDay: isAllDay,
const [title, setTitle] = useState<string>("");
const [isAllDay, setIsAllDay] = useState(false);
const [isPrivate, setIsPrivate] = useState<boolean>(false);
const [startTime, setStartTime] = useState(() => {
const date = initialDate ?? new Date();
date.setSeconds(0, 0);
return date;
});
const [endTime, setEndTime] = useState(() => {
const date = initialDate
? addHours(initialDate, 1)
: addHours(new Date(), 1);
date.setSeconds(0, 0);
return date;
});
const [startDate, setStartDate] = useState(initialDate ?? new Date());
const [endDate, setEndDate] = useState(initialDate ?? new Date());
const [selectedAttendees, setSelectedAttendees] = useState<string[]>([]);
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
const {mutateAsync: createEvent, isLoading, isError} = useCreateEvent();
const {data: members} = useGetFamilyMembers(true)
if (!selectedNewEventDate) return null;
const formatDateTime = (date?: Date | string) => {
if (!date) return undefined;
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
};
await createEvent(eventData);
const combineDateAndTime = (date: Date, time: Date): Date => {
const combined = new Date(date);
combined.setHours(time.getHours());
combined.setMinutes(time.getMinutes());
combined.setSeconds(0);
combined.setMilliseconds(0);
return combined;
};
close();
};
const handleSave = async () => {
let finalStartDate: Date;
let finalEndDate: Date;
const getRepeatLabel = () => {
const selectedDays = repeatInterval;
const allDays = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
];
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
if (isAllDay) {
finalStartDate = new Date(startDate.setHours(0, 0, 0, 0));
finalEndDate = new Date(startDate.setHours(0, 0, 0, 0));
} else {
finalStartDate = combineDateAndTime(startDate, startTime);
finalEndDate = combineDateAndTime(endDate, endTime);
}
const isEveryWorkDay = workDays.every((day) => selectedDays.includes(day));
const eventData: Partial<EventData> = {
title: title,
startDate: finalStartDate,
endDate: finalEndDate,
allDay: isAllDay,
attendees: selectedAttendees,
};
const isEveryDay = allDays.every((day) => selectedDays.includes(day));
await createEvent(eventData);
if (isEveryDay) {
return "Every day";
} else if (
isEveryWorkDay &&
!selectedDays.includes("saturday") &&
!selectedDays.includes("sunday")
) {
return "Every work day";
} else {
return selectedDays
.map((item) => daysOfWeek.find((day) => day.value === item)?.label)
.join(", ");
close();
};
const getRepeatLabel = () => {
const selectedDays = repeatInterval;
const allDays = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
];
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
const isEveryWorkDay = workDays.every((day) => selectedDays.includes(day));
const isEveryDay = allDays.every((day) => selectedDays.includes(day));
if (isEveryDay) {
return "Every day";
} else if (
isEveryWorkDay &&
!selectedDays.includes("saturday") &&
!selectedDays.includes("sunday")
) {
return "Every work day";
} else {
return selectedDays
.map((item) => daysOfWeek.find((day) => day.value === item)?.label)
.join(", ");
}
};
if (isLoading && !isError) {
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<LoaderScreen message={"Saving event..."} color={Colors.grey40}/>
</Modal>
);
}
};
if (isLoading && !isError) {
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<LoaderScreen message={"Saving event..."} color={Colors.grey40} />
</Modal>
);
}
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<View
style={{
flex: 1,
backgroundColor: "#fff",
paddingTop: insets.top, // Safe area inset for top
paddingBottom: insets.bottom, // Safe area inset for bottom
paddingLeft: insets.left, // Safe area inset for left
paddingRight: insets.right, // Safe area inset for right
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
padding: 16,
}}
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<TouchableOpacity onPress={close}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Cancel
</Text>
</TouchableOpacity>
<DropModalIcon onPress={close} />
<TouchableOpacity onPress={handleSave}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Save
</Text>
</TouchableOpacity>
</View>
<ScrollView>
<TextField
placeholder="Add event title"
value={title}
onChangeText={(text) => {
setTitle(text);
}}
placeholderTextColor="#2d2d30"
style={{ fontFamily: "Manrope_500Medium", fontSize: 22 }}
paddingT-15
paddingL-30
/>
<View style={styles.divider} marginT-8 />
<View marginL-30 centerV>
<View row spread marginB-10 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191" />
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
All day
</Text>
</View>
<View right marginR-30>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isAllDay}
onValueChange={(value) => setIsAllDay(value)}
/>
</View>
</View>
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={startDate}
onChange={(date) => {
setStartDate(date);
}}
maximumDate={endDate}
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-8
/>
</View>
<DateTimePicker
value={startTime}
onChange={(date) => setStartTime(date)}
maximumDate={endTime}
minuteInterval={5}
dateTimeFormatter={(date, mode) =>
date.toLocaleTimeString("en-us", {
hour: "numeric",
minute: "numeric",
})
}
mode="time"
<View
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
flex: 1,
backgroundColor: "#fff",
paddingTop: insets.top, // Safe area inset for top
paddingBottom: insets.bottom, // Safe area inset for bottom
paddingLeft: insets.left, // Safe area inset for left
paddingRight: insets.right, // Safe area inset for right
}}
marginR-30
/>
</View>
{!isAllDay && (
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={endDate}
minimumDate={startDate}
text70
marginL-8
onChange={(date) => {
setEndDate(date);
}}
>
<View
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
flexDirection: "row",
justifyContent: "space-between",
padding: 16,
}}
/>
>
<TouchableOpacity onPress={close}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Cancel
</Text>
</TouchableOpacity>
<DropModalIcon onPress={close}/>
<TouchableOpacity onPress={handleSave}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Save
</Text>
</TouchableOpacity>
</View>
<DateTimePicker
value={endTime}
onChange={(date) => setEndTime(date)}
minimumDate={startTime}
minuteInterval={5}
dateTimeFormatter={(date, mode) =>
date.toLocaleTimeString("en-us", {
hour: "numeric",
minute: "numeric",
})
}
mode="time"
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginR-30
<ScrollView>
<TextField
placeholder="Add event title"
value={title}
autoFocus
onChangeText={(text) => {
setTitle(text);
}}
placeholderTextColor="#2d2d30"
style={{fontFamily: "Manrope_500Medium", fontSize: 22}}
paddingT-15
paddingL-30
returnKeyType="next"
/>
<View style={styles.divider} marginT-8/>
<View marginL-30 centerV>
<View row spread marginB-10 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191"/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
All day
</Text>
</View>
<View right marginR-30>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isAllDay}
onValueChange={(value) => setIsAllDay(value)}
/>
</View>
</View>
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191"/>
<DateTimePicker
value={startDate}
onChange={(date) => {
setStartDate(date);
}}
maximumDate={endDate}
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-8
/>
</View>
<DateTimePicker
value={startTime}
onChange={(date) => setStartTime(date)}
maximumDate={endTime}
minuteInterval={5}
dateTimeFormatter={(date, mode) =>
date.toLocaleTimeString("en-us", {
hour: "numeric",
minute: "numeric",
})
}
mode="time"
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginR-30
/>
</View>
{!isAllDay && (
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191"/>
<DateTimePicker
value={endDate}
minimumDate={startDate}
text70
marginL-8
onChange={(date) => {
setEndDate(date);
}}
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
/>
</View>
<DateTimePicker
value={endTime}
onChange={(date) => setEndTime(date)}
minimumDate={startTime}
minuteInterval={5}
dateTimeFormatter={(date, mode) =>
date.toLocaleTimeString("en-us", {
hour: "numeric",
minute: "numeric",
})
}
mode="time"
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginR-30
/>
</View>
)}
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191"/>
<Text
style={{fontFamily: "Manrope_600SemiBold", fontSize: 18}}
marginL-10
>
Attendees
</Text>
<View flex-1/>
<Picker
value={selectedAttendees}
onChange={(value) => setSelectedAttendees(value as string[] ?? [])}
style={{marginLeft: "auto"}}
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="Add"
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
/>
}>
{members?.map((member) => (
<Picker.Item
key={member?.uid}
value={member?.uid!}
label={member?.firstName + " " + member?.lastName}
/>
))}
</Picker>
</View>
<View marginL-35>
<AssigneesDisplay setSlectedAttendees={setSelectedAttendees} selectedAttendees={selectedAttendees}/>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon/>
<Text
style={{
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
}}
marginL-10
>
Reminders
</Text>
</View>
<View>
<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,
}}
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
color="#ea156c"
label="Set Reminder"
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Mark as Private
</Text>
</View>
<View>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isPrivate}
onValueChange={(value) => setIsPrivate(value)}
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Add Details
</Text>
</View>
<View></View>
</View>
</ScrollView>
<Button
marginH-30
marginB-15
label="Create event from image"
text70
style={{height: 47}}
labelStyle={{fontFamily: "PlusJakartaSans_500Medium", fontSize: 15}}
backgroundColor="#05a8b6"
iconSource={() => (
<View marginR-5>
<CameraIcon color="white"/>
</View>
)}
/>
</View>
)}
</View>
<View style={styles.divider} />
<View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191" />
<Text
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 18 }}
marginL-10
>
Attendees
</Text>
<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="Add"
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
/>
</View>
<View marginL-35>
<AssigneesDisplay />
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon />
<Text
style={{
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
}}
marginL-10
>
Reminders
</Text>
</View>
<View>
<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,
}}
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
color="#ea156c"
label="Set Reminder"
/>
</View>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon />
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Mark as Private
</Text>
</View>
<View>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isPrivate}
onValueChange={(value) => setIsPrivate(value)}
/>
</View>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon />
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Add Details
</Text>
</View>
<View></View>
</View>
</ScrollView>
<Button
marginH-30
marginB-15
label="Create event from image"
text70
style={{ height: 47 }}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 15 }}
backgroundColor="#05a8b6"
iconSource={() => (
<View marginR-5>
<CameraIcon color="white" />
</View>
)}
/>
</View>
</Modal>
);
</Modal>
);
};
const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 },
gradient: {
height: "25%",
position: "absolute",
bottom: 0,
width: "100%",
},
buttonContainer: {
position: "absolute",
bottom: 25,
width: "100%",
},
button: {
backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20,
},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
gradient: {
height: "25%",
position: "absolute",
bottom: 0,
width: "100%",
},
buttonContainer: {
position: "absolute",
bottom: 25,
width: "100%",
},
button: {
backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20,
},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
});

View File

@ -0,0 +1,9 @@
import { atom } from 'jotai';
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
export const editVisibleAtom = atom<boolean>(false);
export const eventForEditAtom = atom<CalendarEvent | undefined>(undefined);
export const isFamilyViewAtom = atom<boolean>(false);
export const modeAtom = atom<"week" | "month" | "day">("week");
export const selectedDateAtom = atom<Date>(new Date());
export const selectedNewEventDateAtom = atom<Date | undefined>(undefined);

View File

@ -0,0 +1,20 @@
export const modeMap = new Map([
[0, "day"],
[1, "week"],
[2, "month"],
]);
export const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];

View File

@ -0,0 +1,13 @@
export interface CalendarEvent {
id?: number | string; // Unique identifier for the event
user?: string;
title: string; // Event title or name
description?: string; // Optional description for the event
start: Date; // Start date and time of the event
end: Date; // End date and time of the event
location?: string; // Optional event location
allDay?: boolean; // Specifies if the event lasts all day
eventColor?: string; // Optional color to represent the event
participants?: string[]; // Optional list of participants or attendees
private?: boolean;
}

View File

@ -23,7 +23,7 @@ const CategoryDropdown = (props: {
padding: 10,
}}
>
<Text>{category}</Text>
<Text style={{fontFamily: "Manrope_400Regular"}}>{category}</Text>
</View>
</TouchableOpacity>
))}

View File

@ -1,7 +1,8 @@
import {Text, View} from "react-native";
import React, {useEffect, useRef} from "react";
import React, {useEffect, useRef, useState} from "react";
import {TextField, TextFieldRef} from "react-native-ui-lib";
import {GroceryCategory, useGroceryContext,} from "@/contexts/GroceryContext";
import CategoryDropdown from "./CategoryDropdown";
interface IEditGrocery {
id?: string;
@ -17,6 +18,7 @@ interface IEditGrocery {
const EditGroceryItem = ({editGrocery}: { editGrocery: IEditGrocery }) => {
const {fuzzyMatchGroceryCategory} = useGroceryContext();
const inputRef = useRef<TextFieldRef>(null);
const [category, setCategory] = useState<GroceryCategory>(GroceryCategory.None);
useEffect(() => {
if (editGrocery.setCategory)

View File

@ -1,17 +1,17 @@
import {Button, ButtonSize, Dialog, Text, TextField, View} from "react-native-ui-lib";
import React, {useEffect, useState} from "react";
import React, {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';
import {BarCodeScanner} from "expo-barcode-scanner";
const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> }) => {
const SignInPage = ({setTab}: {
setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">>
}) => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [scanned, setScanned] = useState<boolean>(false);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const {mutateAsync: signIn, error, isError} = useSignIn();
@ -19,7 +19,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
const handleSignIn = async () => {
await signIn({email, password});
if(!isError) {
if (!isError) {
Toast.show({
type: "success",
text1: "Login successful!"
@ -33,10 +33,10 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
}
};
const handleQrCodeScanned = async ({ data }: { data: string }) => {
const handleQrCodeScanned = async ({data}: { data: string }) => {
setShowCameraDialog(false);
try {
await signInWithQrCode({ userId: data });
await signInWithQrCode({userId: data});
Toast.show({
type: "success",
text1: "Login successful with QR code!"
@ -51,9 +51,9 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
};
const getCameraPermissions = async (callback: () => void) => {
const { status } = await Camera.requestCameraPermissionsAsync();
const {status} = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted');
if(status === 'granted') {
if (status === 'granted') {
callback();
}
};
@ -83,12 +83,12 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
label="Login with a QR Code"
onPress={() => {
getCameraPermissions(() => setShowCameraDialog(true));
}
}
}
}
style={{marginBottom: 20}}
backgroundColor="#fd1775"
/>
{isError && <Text center style={{marginBottom: 20}}>{`${error}`}</Text>}
{isError && <Text center style={{marginBottom: 20}}>{`${error?.toString()?.split("]")?.[1]}`}</Text>}
<View row centerH marginB-5 gap-5>
<Text text70>
@ -131,7 +131,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
bottom
width="100%"
height="70%"
containerStyle={{ padding: 0 }}
containerStyle={{padding: 0}}
>
{hasPermission === null ? (
<Text>Requesting camera permissions...</Text>
@ -139,7 +139,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
<Text>No access to camera</Text>
) : (
<CameraView
style={{ flex: 1 }}
style={{flex: 1}}
onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{
barcodeTypes: ["qr"],
@ -150,7 +150,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
label="Cancel"
onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775"
style={{ margin: 10 }}
style={{margin: 10}}
/>
</Dialog>
</View>

View File

@ -1,10 +1,11 @@
import React, { useState } from "react";
import React, { useRef, useState } from "react";
import {
Button,
ButtonSize,
Checkbox,
Text,
TextField,
TextFieldRef,
TouchableOpacity,
View,
} from "react-native-ui-lib";
@ -13,7 +14,13 @@ import { ProfileType } from "@/contexts/AuthContext";
import { StyleSheet } from "react-native";
import { AntDesign } from "@expo/vector-icons";
const SignUpPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> }) => {
const SignUpPage = ({
setTab,
}: {
setTab: React.Dispatch<
React.SetStateAction<"register" | "login" | "reset-password">
>;
}) => {
const [email, setEmail] = useState<string>("");
const [firstName, setFirstName] = useState<string>("");
const [lastName, setLastName] = useState<string>("");
@ -24,6 +31,10 @@ const SignUpPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
const { mutateAsync: signUp } = useSignUp();
const lnameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null);
const passwordRef = useRef<TextFieldRef>(null);
const handleSignUp = async () => {
await signUp({ email, password, firstName, lastName });
};
@ -36,24 +47,34 @@ const SignUpPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
<Text center>Please enter your details.</Text>
<TextField
marginT-60
autoFocus
placeholder="First name"
value={firstName}
onChangeText={setFirstName}
style={styles.textfield}
onSubmitEditing={() => {lnameRef.current?.focus()}}
blurOnSubmit={false}
/>
<TextField
ref={lnameRef}
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
style={styles.textfield}
onSubmitEditing={() => {emailRef.current?.focus()}}
blurOnSubmit={false}
/>
<TextField
ref={emailRef}
placeholder="Email"
value={email}
onChangeText={setEmail}
style={styles.textfield}
onSubmitEditing={() => {passwordRef.current?.focus()}}
blurOnSubmit={false}
/>
<TextField
ref={passwordRef}
placeholder="Password"
value={password}
onChangeText={setPassword}

View File

@ -1,14 +1,11 @@
import { AntDesign, Ionicons } from "@expo/vector-icons";
import React, { useCallback, useEffect, useState } from "react";
import { Button, Checkbox, Text, View } from "react-native-ui-lib";
import { ScrollView, StyleSheet } from "react-native";
import { colorMap } from "@/contexts/SettingsContext";
import { TouchableOpacity } from "react-native-gesture-handler";
import { fetchGoogleCalendarEvents } from "@/calendar-integration/google-calendar-utils";
import { fetchMicrosoftCalendarEvents } from "@/calendar-integration/microsoft-calendar-utils";
import { useCreateEventFromProvider } from "@/hooks/firebase/useCreateEvent";
import { useAuthContext } from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
import {AntDesign, Ionicons} from "@expo/vector-icons";
import React, {useCallback, useEffect, useState} from "react";
import {Button, Checkbox, Text, View} from "react-native-ui-lib";
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
import {colorMap} from "@/contexts/SettingsContext";
import {TouchableOpacity} from "react-native-gesture-handler";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
import debounce from "debounce";
import AppleIcon from "@/assets/svgs/AppleIcon";
import GoogleIcon from "@/assets/svgs/GoogleIcon";
@ -16,418 +13,575 @@ import OutlookIcon from "@/assets/svgs/OutlookIcon";
import * as AuthSession from "expo-auth-session";
import * as Google from "expo-auth-session/providers/google";
import * as WebBrowser from "expo-web-browser";
import {UserProfile} from "@firebase/auth";
import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
import * as AppleAuthentication from 'expo-apple-authentication';
import ExpoLocalization from "expo-localization/src/ExpoLocalization";
const googleConfig = {
androidClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
iosClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
webClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
scopes: [
"email",
"profile",
"https://www.googleapis.com/auth/calendar.events.owned",
],
androidClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
iosClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
webClientId:
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
scopes: [
"email",
"profile",
"https://www.googleapis.com/auth/calendar.events.owned",
],
};
const microsoftConfig = {
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Replace with your Microsoft client ID
redirectUri: AuthSession.makeRedirectUri({ path: "settings" }), // Generate redirect URI automatically for Expo
scopes: [
"openid",
"profile",
"email",
"offline_access",
"Calendars.ReadWrite", // Scope for reading calendar events
],
authorizationEndpoint:
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
redirectUri: AuthSession.makeRedirectUri({path: "settings"}),
scopes: [
"openid",
"profile",
"email",
"offline_access",
"Calendars.ReadWrite",
"User.Read"
],
authorizationEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
};
const CalendarSettingsPage = (props: {
setSelectedPage: (page: number) => void;
setSelectedPage: (page: number) => void;
}) => {
const [startDate, setStartDate] = useState<boolean>(false);
const { profileData } = useAuthContext();
const {profileData} = useAuthContext();
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(profileData?.firstDayOfWeek ?? ExpoLocalization.getCalendars()[0].firstWeekday === 1 ? "Mondays" : "Sundays");
const [selectedColor, setSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
const [previousSelectedColor, setPreviousSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
const { mutateAsync: createEventFromProvider } = useCreateEventFromProvider();
const { mutateAsync: updateUserData } = useUpdateUserData();
WebBrowser.maybeCompleteAuthSession();
const [request, response, promptAsync] = Google.useAuthRequest(googleConfig);
useEffect(() => {
signInWithGoogle();
}, [response]);
const fetchAndSaveGoogleEvents = () => {
console.log("fetch");
const timeMin = new Date(new Date().setHours(0, 0, 0, 0));
const timeMax = new Date(
new Date(new Date().setHours(0, 0, 0, 0)).setDate(timeMin.getDate() + 30)
const [selectedColor, setSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
const [previousSelectedColor, setPreviousSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
fetchGoogleCalendarEvents(
profileData?.googleToken,
timeMin.toISOString().slice(0, -5) + "Z",
timeMax.toISOString().slice(0, -5) + "Z"
).then((response) => {
response?.forEach((item) => saveData(item));
});
};
const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
const {mutateAsync: fetchAndSaveOutlookEvents, isLoading: isSyncingOutlook} = useFetchAndSaveOutlookEvents();
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} = useFetchAndSaveAppleEvents();
async function saveData(item: any) {
await createEventFromProvider(item);
}
const fetchAndSaveMicrosoftEvents = () => {
const startDateTime = new Date(new Date().setHours(0, 0, 0, 0));
const endDateTime = new Date(
new Date(new Date().setHours(0, 0, 0, 0)).setDate(
startDateTime.getDate() + 30
)
WebBrowser.maybeCompleteAuthSession();
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
useEffect(() => {
signInWithGoogle();
}, [response]);
const signInWithGoogle = async () => {
try {
if (response?.type === "success") {
const accessToken = response.authentication?.accessToken;
const userInfoResponse = await fetch(
"https://www.googleapis.com/oauth2/v3/userinfo",
{
headers: {Authorization: `Bearer ${accessToken}`},
}
);
const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email;
await updateUserData({
newUserData: {googleToken: accessToken, googleMail: googleMail},
});
await fetchAndSaveGoogleEvents(accessToken, googleMail)
}
} catch (error) {
console.error("Error during Google sign-in:", error);
}
};
const handleMicrosoftSignIn = async () => {
try {
console.log("Starting Microsoft sign-in...");
const authRequest = new AuthSession.AuthRequest({
clientId: microsoftConfig.clientId,
scopes: microsoftConfig.scopes,
redirectUri: microsoftConfig.redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true, // Enable PKCE
});
console.log("Auth request created:", authRequest);
const authResult = await authRequest.promptAsync({
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
});
console.log("Auth result:", authResult);
if (authResult.type === "success" && authResult.params?.code) {
const code = authResult.params.code;
console.log("Authorization code received:", code);
// Exchange authorization code for tokens
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `client_id=${
microsoftConfig.clientId
}&redirect_uri=${encodeURIComponent(
microsoftConfig.redirectUri
)}&grant_type=authorization_code&code=${code}&code_verifier=${
authRequest.codeVerifier
}&scope=${encodeURIComponent(
"https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
)}`,
});
console.log("Token response status:", tokenResponse.status);
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error("Token exchange failed:", errorText);
return;
}
const tokenData = await tokenResponse.json();
console.log("Token data received:", tokenData);
if (tokenData?.access_token) {
console.log("Access token received, fetching user info...");
// Fetch user info from Microsoft Graph API to get the email
const userInfoResponse = await fetch("https://graph.microsoft.com/v1.0/me", {
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const userInfo = await userInfoResponse.json();
console.log("User info received:", userInfo);
if (userInfo.error) {
console.error("Error fetching user info:", userInfo.error);
} else {
const outlookMail = userInfo.mail || userInfo.userPrincipalName;
// Update user data with Microsoft token and email
await updateUserData({
newUserData: {microsoftToken: tokenData.access_token, outlookMail: outlookMail},
});
await fetchAndSaveOutlookEvents(tokenData.access_token, outlookMail)
console.log("User data updated successfully.");
}
}
} else {
console.warn("Authentication was not successful:", authResult);
}
} catch (error) {
console.error("Error during Microsoft sign-in:", error);
}
};
const handleAppleSignIn = async () => {
try {
console.log("Starting Apple Sign-in...");
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
});
console.log("Apple sign-in result:", credential);
const appleToken = credential.identityToken;
const appleMail = credential.email;
if (appleToken) {
console.log("Apple ID token received. Fetch user info if needed...");
// Example: Store user token and email
await updateUserData({
newUserData: {appleToken, appleMail},
});
console.log("User data updated with Apple ID token.");
} else {
console.warn("Apple authentication was not successful or email was hidden.");
}
} catch (error) {
console.error("Error during Apple Sign-in:", error);
}
};
const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => {
try {
await updateUserData({
newUserData: {
eventColor: color,
},
});
} catch (error) {
console.error("Failed to update color:", error);
setSelectedColor(previousSelectedColor);
}
}, 500),
[]
);
fetchMicrosoftCalendarEvents(
profileData?.microsoftToken,
startDateTime.toISOString().slice(0, -5) + "Z",
endDateTime.toISOString().slice(0, -5) + "Z"
).then((response) => {
console.log(response);
response?.forEach((item) => saveData(item));
});
};
const debouncedUpdateFirstDayOfWeek = useCallback(
debounce(async (firstDayOfWeek: string) => {
try {
await updateUserData({
newUserData: {
firstDayOfWeek,
},
});
} catch (error) {
console.error("Failed to update first day of week:", error);
}
}, 500),
[]
);
const signInWithGoogle = async () => {
try {
// Attempt to retrieve user information from AsyncStorage
if (response?.type === "success") {
console.log(response.authentication);
await updateUserData({
newUserData: { googleToken: response.authentication?.accessToken },
});
}
} catch (error) {
// Handle any errors that occur during AsyncStorage retrieval or other operations
console.error("Error retrieving user data from AsyncStorage:", error);
const handleChangeFirstDayOfWeek = () => {
setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
debouncedUpdateFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
}
};
const handleMicrosoftSignIn = async () => {
try {
console.log("Starting Microsoft sign-in...");
const handleChangeColor = (color: string) => {
setPreviousSelectedColor(selectedColor);
setSelectedColor(color);
debouncedUpdateUserData(color);
};
const authRequest = new AuthSession.AuthRequest({
clientId: microsoftConfig.clientId,
scopes: microsoftConfig.scopes,
redirectUri: microsoftConfig.redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true, // Enable PKCE
});
console.log("Auth request created:", authRequest);
const authResult = await authRequest.promptAsync({
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
});
console.log("Auth result:", authResult);
if (authResult.type === "success" && authResult.params?.code) {
const code = authResult.params.code;
console.log("Authorization code received:", code);
// Exchange authorization code for tokens
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `client_id=${
microsoftConfig.clientId
}&redirect_uri=${encodeURIComponent(
microsoftConfig.redirectUri
)}&grant_type=authorization_code&code=${code}&code_verifier=${
authRequest.codeVerifier
}&scope=${encodeURIComponent(
"https://graph.microsoft.com/Calendars.ReadWrite offline_access"
)}`,
});
console.log("Token response status:", tokenResponse.status);
if (!tokenResponse.ok) {
console.error("Token exchange failed:", await tokenResponse.text());
return;
const clearToken = async (provider: "google" | "outlook" | "apple") => {
const newUserData: Partial<UserProfile> = {};
if (provider === "google") {
newUserData.googleToken = null;
newUserData.googleMail = null;
} else if (provider === "outlook") {
newUserData.microsoftToken = null;
newUserData.outlookMail = null;
} else if (provider === "apple") {
newUserData.appleToken = null;
newUserData.appleMail = null;
}
await updateUserData({newUserData});
};
const tokenData = await tokenResponse.json();
console.log("Token data received:", tokenData);
if (tokenData?.id_token) {
console.log("ID token received, updating user data...");
await updateUserData({
newUserData: { microsoftToken: tokenData.access_token },
});
console.log("User data updated successfully.");
}
} else {
console.warn("Authentication was not successful:", authResult);
}
} catch (error) {
console.error("Error during Microsoft sign-in:", error);
}
};
const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => {
try {
await updateUserData({
newUserData: {
eventColor: color,
},
});
} catch (error) {
console.error("Failed to update color:", error);
setSelectedColor(previousSelectedColor);
}
}, 500),
[]
);
const handleChangeColor = (color: string) => {
setPreviousSelectedColor(selectedColor);
setSelectedColor(color);
debouncedUpdateUserData(color);
};
return (
<ScrollView>
<View marginH-30>
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
<View row marginT-20 marginB-35 centerV>
<Ionicons
name="chevron-back"
size={14}
color="#979797"
style={{ paddingBottom: 3 }}
/>
<Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
color="#979797"
>
Return to main settings
</Text>
</View>
</TouchableOpacity>
<Text style={styles.subTitle}>Calendar settings</Text>
<View style={styles.card}>
<Text style={styles.cardTitle} marginB-14>
Event Color Preference
</Text>
<View row spread>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.pink)}>
<View style={styles.colorBox} backgroundColor={colorMap.pink}>
{selectedColor == colorMap.pink && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.orange)}
>
<View style={styles.colorBox} backgroundColor={colorMap.orange}>
{selectedColor == colorMap.orange && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)}>
<View style={styles.colorBox} backgroundColor={colorMap.green}>
{selectedColor == colorMap.green && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)}>
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
{selectedColor == colorMap.teal && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.purple)}
>
<View style={styles.colorBox} backgroundColor={colorMap.purple}>
{selectedColor == colorMap.purple && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
</View>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>Weekly Start Date</Text>
<View row marginV-5 marginT-20>
<Checkbox
value={startDate}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(true)}
/>
<View row marginL-8>
<Text text70>Sundays</Text>
<Text text70 color="gray">
{" "}
(default)
</Text>
</View>
</View>
<View row marginV-5>
<Checkbox
value={!startDate}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => setStartDate(false)}
/>
<Text text70 marginL-8>
Mondays
</Text>
</View>
</View>
<Text style={styles.subTitle} marginT-30 marginB-25>
Add Calendar
</Text>
<Button
onPress={() => promptAsync()}
label="Connect Google"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<GoogleIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
label="Connect Apple"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<AppleIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
onPress={handleMicrosoftSignIn}
label="Connect Outlook"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<OutlookIcon />
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Text style={styles.subTitle} marginT-30 marginB-20>
Connected Calendars
</Text>
<View style={styles.card}>
<View style={{ marginTop: 20 }}>
<Button
onPress={fetchAndSaveGoogleEvents}
label="Sync Google"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<GoogleIcon />
return (
<ScrollView>
<View marginH-30 marginB-30>
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
<View row marginT-20 marginB-35 centerV>
<Ionicons
name="chevron-back"
size={14}
color="#979797"
style={{paddingBottom: 3}}
/>
<Text
style={{fontFamily: "Poppins_400Regular", fontSize: 14.71}}
color="#979797"
>
Return to main settings
</Text>
</View>
</TouchableOpacity>
<Text style={styles.subTitle}>Calendar settings</Text>
<View style={styles.card}>
<Text style={styles.cardTitle} marginB-14>
Event Color Preference
</Text>
<View row spread>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.pink)}>
<View style={styles.colorBox} backgroundColor={colorMap.pink}>
{selectedColor == colorMap.pink && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.orange)}
>
<View style={styles.colorBox} backgroundColor={colorMap.orange}>
{selectedColor == colorMap.orange && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)}>
<View style={styles.colorBox} backgroundColor={colorMap.green}>
{selectedColor == colorMap.green && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)}>
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
{selectedColor == colorMap.teal && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.purple)}
>
<View style={styles.colorBox} backgroundColor={colorMap.purple}>
{selectedColor == colorMap.purple && (
<AntDesign name="check" size={30} color="white"/>
)}
</View>
</TouchableOpacity>
</View>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
onPress={fetchAndSaveMicrosoftEvents}
label="Sync Outlook"
labelStyle={styles.addCalLbl}
iconSource={() => (
<View marginR-15>
<OutlookIcon />
<View style={styles.card}>
<Text style={styles.cardTitle}>Weekly Start Date</Text>
<View row marginV-5 marginT-20>
<Checkbox
value={firstDayOfWeek === "Sundays"}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => handleChangeFirstDayOfWeek("Sundays")}
/>
<View row marginL-8>
<Text text70>Sundays</Text>
<Text text70 color="gray">
{" "}
(default)
</Text>
</View>
</View>
<View row marginV-5>
<Checkbox
value={firstDayOfWeek === "Mondays"}
style={styles.checkbox}
color="#ea156d"
onValueChange={() => handleChangeFirstDayOfWeek("Mondays")}
/>
<Text text70 marginL-8>
Mondays
</Text>
</View>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
</View>
</View>
</View>
</ScrollView>
);
<Text style={styles.subTitle} marginT-30 marginB-25>
Add Calendar
</Text>
<Button
onPress={() => !profileData?.googleToken ? promptAsync() : clearToken("google")}
label={profileData?.googleToken ? `Disconnect ${profileData.googleMail}` : "Connect Google"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2
}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
onPress={() => !profileData?.appleToken ? handleAppleSignIn() : clearToken("google")}
label={profileData?.appleToken ? `Disconnect ${profileData.appleMail}` : "Connect Apple"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2
}}
iconSource={() => (
<View marginR-15>
<AppleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
<Button
onPress={() => !profileData?.microsoftToken ? handleMicrosoftSignIn() : clearToken("outlook")}
label={profileData?.microsoftToken ? `Disconnect ${profileData.outlookMail}` : "Connect Outlook"}
labelStyle={styles.addCalLbl}
labelProps={{
numberOfLines: 2
}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{(profileData?.googleMail || profileData?.outlookMail || profileData?.appleMail) && (
<>
<Text style={styles.subTitle} marginT-30 marginB-20>
Connected Calendars
</Text>
<View style={styles.noPaddingCard}>
<View style={{marginTop: 20}}>
{!!profileData?.googleMail && (
<TouchableOpacity
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingGoogle}
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)}
label={`Sync ${profileData?.googleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<GoogleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingGoogle ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
{!!profileData?.appleMail && (
<TouchableOpacity>
<View row paddingR-20 center>
<Button
disabled={isSyncingApple}
onPress={() => fetchAndSaveAppleEvents(undefined, undefined)}
label={`Sync ${profileData?.appleMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<AppleIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingApple ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
{!!profileData?.outlookMail && (
<TouchableOpacity
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)}
>
<View row paddingR-20 center>
<Button
disabled={isSyncingOutlook}
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)}
label={`Sync ${profileData?.outlookMail}`}
labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}}
iconSource={() => (
<View marginR-15>
<OutlookIcon/>
</View>
)}
style={styles.addCalBtn}
color="black"
text70BL
/>
{isSyncingOutlook ? (
<ActivityIndicator/>
) : (
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
)}
</View>
</TouchableOpacity>
)}
</View>
</View>
</>
)}
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
addCalBtn: {
backgroundColor: "#ffffff",
marginBottom: 15,
justifyContent: "flex-start",
paddingLeft: 25,
},
backBtn: {
backgroundColor: "red",
marginLeft: -2,
justifyContent: "flex-start",
},
card: {
backgroundColor: "white",
width: "100%",
padding: 20,
paddingBottom: 30,
marginTop: 20,
borderRadius: 12,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
width: 51,
borderRadius: 12,
},
checkbox: {
borderRadius: 50,
},
addCalLbl: {
fontSize: 16,
fontFamily: "PlusJakartaSan_500Medium",
},
subTitle: {
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
},
cardTitle: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
addCalBtn: {
backgroundColor: "#ffffff",
marginBottom: 15,
justifyContent: "flex-start",
paddingLeft: 25,
},
backBtn: {
backgroundColor: "red",
marginLeft: -2,
justifyContent: "flex-start",
},
card: {
backgroundColor: "white",
width: "100%",
padding: 20,
paddingBottom: 30,
marginTop: 20,
borderRadius: 12,
},
noPaddingCard: {
backgroundColor: "white",
width: "100%",
marginTop: 20,
borderRadius: 12,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
width: 51,
borderRadius: 12,
},
checkbox: {
borderRadius: 50,
},
addCalLbl: {
fontSize: 16,
fontFamily: "PlusJakartaSan_500Medium",
flexWrap: "wrap",
width: "75%",
textAlign: "left",
lineHeight: 20,
overflow: "visible"
},
subTitle: {
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
},
cardTitle: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
});
export default CalendarSettingsPage;

View File

@ -1,105 +1,118 @@
import { View, Text, Button } from "react-native-ui-lib";
import React, { useState } from "react";
import { StyleSheet } from "react-native";
import { Entypo, Ionicons, Octicons } from "@expo/vector-icons";
import {Button, Text, View} from "react-native-ui-lib";
import React, {useState} from "react";
import {StyleSheet} from "react-native";
import {Octicons} from "@expo/vector-icons";
import CalendarSettingsPage from "./CalendarSettingsPage";
import ChoreRewardSettings from "./ChoreRewardSettings";
import UserSettings from "./UserSettings";
import { AuthContextProvider } from "@/contexts/AuthContext";
import ProfileIcon from "@/assets/svgs/ProfileIcon";
import CalendarIcon from "@/assets/svgs/CalendarIcon";
import PrivacyPolicyIcon from "@/assets/svgs/PrivacyPolicyIcon";
import ArrowRightIcon from "@/assets/svgs/ArrowRightIcon";
const pageIndex = {
main: 0,
user: 1,
calendar: 2,
chore: 3,
policy: 4,
main: 0,
user: 1,
calendar: 2,
chore: 3,
policy: 4,
};
const SettingsPage = () => {
const [selectedPage, setSelectedPage] = useState<number>(0);
return (
<View flexG>
{selectedPage == 0 && (
<View flexG centerH marginH-30 marginT-30>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Manage My Profile"
labelStyle={styles.label}
color="#07b8c7"
iconSource={() => (
<ProfileIcon style={{marginRight: 10}} color="#07b9c8" />
)}
onPress={() => setSelectedPage(pageIndex.user)}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Calendar Settings"
labelStyle={styles.label}
color="#fd1775"
iconSource={() => (
<CalendarIcon style={{marginRight: 10}}/>
)}
onPress={() => {
setSelectedPage(pageIndex.calendar);
}}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="To-Do Reward Settings"
labelStyle={styles.label}
color="#ff9900"
iconSource={() => (
<Octicons
name="gear"
size={24}
color="#ff9900"
style={{ marginRight: 10 }}
/>
)}
onPress={() => setSelectedPage(pageIndex.chore)}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
label="Cally Privacy Policy"
labelStyle={styles.label}
iconSource={() => (
<PrivacyPolicyIcon style={{marginRight: 10}}/>
)}
color="#6c645b"
/>
</View>
)}
{selectedPage == pageIndex.calendar && (
<CalendarSettingsPage setSelectedPage={setSelectedPage} />
)}
{selectedPage == pageIndex.chore && (
<ChoreRewardSettings setSelectedPage={setSelectedPage} />
)}
{selectedPage == pageIndex.user && (
<UserSettings setSelectedPage={setSelectedPage} />
)}
</View>
);
const [selectedPage, setSelectedPage] = useState<number>(0);
return (
<View flexG>
{selectedPage == 0 && (
<View flexG centerH marginH-30 marginT-30>
<Button
backgroundColor="white"
style={styles.mainBtn}
children={
<View row centerV width={"100%"}>
<ProfileIcon style={{marginRight: 10}} color="#07b9c8"/>
<Text style={styles.label} color="#07b8c7">
Manage My Profile
</Text>
<ArrowRightIcon style={{marginLeft: "auto"}}/>
</View>
}
onPress={() => setSelectedPage(pageIndex.user)}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
children={
<View row centerV width={"100%"}>
<CalendarIcon style={{marginRight: 10}}/>
<Text style={styles.label} color="#fd1775">
Calendar Settings
</Text>
<ArrowRightIcon style={{marginLeft: "auto"}}/>
</View>
}
onPress={() => {
setSelectedPage(pageIndex.calendar);
}}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
children={
<View row centerV width={"100%"}>
<Octicons
name="gear"
size={24}
color="#ff9900"
style={{marginRight: 10}}
/>
<Text style={styles.label} color="#ff9900">
To-Do Reward Settings
</Text>
<ArrowRightIcon style={{marginLeft: "auto"}}/>
</View>
}
onPress={() => setSelectedPage(pageIndex.chore)}
/>
<Button
backgroundColor="white"
style={styles.mainBtn}
children={
<View row centerV width={"100%"}>
<PrivacyPolicyIcon style={{marginRight: 10}}/>
<Text style={styles.label} color="#6c645b">
Cally Privacy Policy
</Text>
<ArrowRightIcon style={{marginLeft: "auto"}}/>
</View>
}
/>
</View>
)}
{selectedPage == pageIndex.calendar && (
<CalendarSettingsPage setSelectedPage={setSelectedPage}/>
)}
{selectedPage == pageIndex.chore && (
<ChoreRewardSettings setSelectedPage={setSelectedPage}/>
)}
{selectedPage == pageIndex.user && (
<UserSettings setSelectedPage={setSelectedPage}/>
)}
</View>
);
};
export default SettingsPage;
const styles = StyleSheet.create({
mainBtn: {
width: "100%",
justifyContent: "flex-start",
marginBottom: 20,
height: 60,
},
label:{
fontFamily: "Poppins_400Regular",
fontSize: 14.71,
textAlignVertical: 'center'
}
mainBtn: {
width: 311,
justifyContent: "flex-start",
marginBottom: 20,
height: 57.61,
},
label: {
fontFamily: "Poppins_400Regular",
fontSize: 14.71,
textAlignVertical: "center",
},
});

View File

@ -5,14 +5,16 @@ import {
Colors,
Dialog,
FloatingButton,
KeyboardAwareScrollView,
PanningProvider,
Picker,
Text,
TextField,
TextFieldRef,
TouchableOpacity,
View,
} from "react-native-ui-lib";
import React, {useState} from "react";
import React, {useEffect, useRef, useState} from "react";
import {ScrollView, StyleSheet} from "react-native";
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
@ -20,6 +22,13 @@ import {ProfileType} from "@/contexts/AuthContext";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import UserMenu from "@/components/pages/settings/user_settings_views/UserMenu";
import {uuidv4} from "@firebase/util";
import QRIcon from "@/assets/svgs/QRIcon";
import EmailIcon from "@/assets/svgs/EmailIcon";
import CircledXIcon from "@/assets/svgs/CircledXIcon";
import ProfileIcon from "@/assets/svgs/ProfileIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import Ionicons from "@expo/vector-icons/Ionicons";
import {PreviousNextView} from "react-native-keyboard-manager";
const MyGroup = () => {
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
@ -31,6 +40,9 @@ const MyGroup = () => {
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const lNameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null);
const [showQRCodeDialog, setShowQRCodeDialog] = useState("");
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
@ -43,10 +55,14 @@ const MyGroup = () => {
const caregivers =
familyMembers?.filter((x) => x.userType === ProfileType.CAREGIVER) ?? [];
const familyDevices =
familyMembers?.filter((x) => x.userType === ProfileType.FAMILY_DEVICE) ?? [];
familyMembers?.filter((x) => x.userType === ProfileType.FAMILY_DEVICE) ??
[];
const handleCreateSubUser = async () => {
if (!firstName || (selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName)) {
if (
!firstName ||
(selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName)
) {
console.error("First name and last name are required");
return;
}
@ -68,23 +84,29 @@ const MyGroup = () => {
password: uuidv4(),
userType: selectedStatus as ProfileType,
});
console.log(res)
console.log(res);
if (!isError) {
setShowNewUserInfoDialog(false);
if(res?.data?.userId) {
if (res?.data?.userId) {
setTimeout(() => {
setShowQRCodeDialog(res.data.userId)
}, 500)
setShowQRCodeDialog(res.data.userId);
}, 500);
}
}
};
useEffect(() => {
setFirstName("");
setLastName("");
setEmail("");
}, [])
// @ts-ignore
return (
<View style={{flex: 1}}>
<View style={{flex: 1, minHeight: 500}}>
<View>
<ScrollView style={styles.card}>
{!parents.length && !children.length && !caregivers.length && (
@ -126,7 +148,11 @@ const MyGroup = () => {
<View flex-1/>
<UserMenu setShowQRCodeDialog={(val) => setShowQRCodeDialog("")} showQRCodeDialog={showQRCodeDialog === member?.uid} userId={member?.uid!}/>
<UserMenu
setShowQRCodeDialog={(val) => setShowQRCodeDialog("")}
showQRCodeDialog={showQRCodeDialog === member?.uid}
userId={member?.uid!}
/>
</Card>
))}
</>
@ -161,7 +187,11 @@ const MyGroup = () => {
</Text>
</View>
<UserMenu setShowQRCodeDialog={(val) => setShowQRCodeDialog("")} showQRCodeDialog={showQRCodeDialog === member?.uid} userId={member?.uid!}/>
<UserMenu
setShowQRCodeDialog={(val) => setShowQRCodeDialog("")}
showQRCodeDialog={showQRCodeDialog === member?.uid}
userId={member?.uid!}
/>
</Card>
))}
</>
@ -188,15 +218,17 @@ const MyGroup = () => {
backgroundColor={Colors.grey60}
/>
<View marginL-10>
<Text text70M>
{member.firstName}
</Text>
<Text text70M>{member.firstName}</Text>
<Text text90 grey40>
Family Device
</Text>
</View>
<UserMenu setShowQRCodeDialog={(val) => setShowQRCodeDialog("")} showQRCodeDialog={showQRCodeDialog === member?.uid} userId={member?.uid!}/>
<UserMenu
setShowQRCodeDialog={(val) => setShowQRCodeDialog("")}
showQRCodeDialog={showQRCodeDialog === member?.uid}
userId={member?.uid!}
/>
</Card>
))}
</>
@ -211,6 +243,7 @@ const MyGroup = () => {
button={{
label: "+ Add a user device",
onPress: () => setShowAddUserDialog(true),
style: styles.bottomButton,
}}
/>
@ -219,13 +252,26 @@ const MyGroup = () => {
onDismiss={() => setShowAddUserDialog(false)}
panDirection={PanningProvider.Directions.DOWN}
>
<Card padding-25 gap-10>
<Text>Add a new user device</Text>
<Card
paddingH-25
paddingT-40
paddingB-20
gap-10
centerH
borderRadius={20}
>
<Text style={styles.dialogTitle} marginB-35>
Add a new user device
</Text>
<Button backgroundColor={"#FD1775"}>
<Text white>Show a QR Code</Text>
<Button backgroundColor={"#FD1775"} style={styles.dialogBtn}>
<QRIcon/>
<Text style={styles.dialogBtnLbl} marginL-7>
Show a QR Code
</Text>
</Button>
<Button
style={styles.dialogBtn}
backgroundColor={"#05A8B6"}
onPress={() => {
setShowAddUserDialog(false);
@ -234,11 +280,18 @@ const MyGroup = () => {
}, 500);
}}
>
<Text white>Enter email address</Text>
<EmailIcon/>
<Text style={styles.dialogBtnLbl} marginL-7>
Enter email address
</Text>
</Button>
<TouchableOpacity onPress={() => setShowAddUserDialog(false)} center>
<Text>Return to user settings</Text>
<TouchableOpacity
onPress={() => setShowAddUserDialog(false)}
center
marginT-30
>
<Text style={styles.dialogBackBtn}>Return to user settings</Text>
</TouchableOpacity>
</Card>
</Dialog>
@ -248,98 +301,167 @@ const MyGroup = () => {
visible={showNewUserInfoDialog}
onDismiss={() => setShowNewUserInfoDialog(false)}
>
<Card padding-25 style={styles.dialogCard}>
<View row spread>
<Text text60M>New User Information</Text>
<TouchableOpacity onPress={() => setShowAddUserDialog(false)}>
<Text>X</Text>
</TouchableOpacity>
</View>
<PreviousNextView>
<KeyboardAwareScrollView>
<Card padding-25 style={styles.dialogCard}>
<View row spread>
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 16}}>
New User Information
</Text>
<TouchableOpacity onPress={() => {
setShowNewUserInfoDialog(false)
}}>
<CircledXIcon/>
</TouchableOpacity>
</View>
<View style={styles.divider} spread/>
<View row centerV gap-20 marginV-20>
<Avatar
imageStyle={{borderRadius: 10}}
containerStyle={{borderRadius: 10}}
size={60}
backgroundColor={Colors.grey60}
/>
<TouchableOpacity onPress={() => {
}}>
<Text style={{color: Colors.green10}}>
Upload User Profile Photo
<View row centerV gap-20 marginV-20>
<View
height={65.54}
width={65.54}
children={
<ProfileIcon color={"#d6d6d6"} width={37} height={37}/>
}
backgroundColor={Colors.grey60}
style={{borderRadius: 25}}
center
/>
<TouchableOpacity onPress={() => {
}}>
<Text color="#50be0c" style={styles.jakarta13} marginL-15>
Upload User Profile Photo
</Text>
</TouchableOpacity>
</View>
<Text style={styles.jakarta12}>Member Status</Text>
<View style={styles.viewPicker}>
<Picker
editable={!isLoading}
value={selectedStatus}
onChange={(item) => setSelectedStatus(item)}
showSearch
floatingPlaceholder
style={styles.inViewPicker}
trailingAccessory={
<View style={{
justifyContent: "center",
alignItems: "center",
height: "100%",
marginTop: -38,
paddingRight: 15
}}>
<Ionicons name={"chevron-down"} style={{alignSelf: "center"}} size={20}
color={"#000000"}/>
</View>
}
>
<Picker.Item label="Child" value={ProfileType.CHILD}/>
<Picker.Item label="Parent" value={ProfileType.PARENT}/>
<Picker.Item label="Caregiver" value={ProfileType.CAREGIVER}/>
<Picker.Item
label="Family Device"
value={ProfileType.FAMILY_DEVICE}
/>
</Picker>
</View>
<Text style={styles.jakarta12}>
{selectedStatus === ProfileType.FAMILY_DEVICE
? "Device Name"
: "First Name"}
</Text>
</TouchableOpacity>
</View>
<Text style={styles.label}>Member Status</Text>
<Picker
editable={!isLoading}
value={selectedStatus}
//@ts-ignore
onChange={(item) => setSelectedStatus(item)}
style={styles.picker}
showSearch
floatingPlaceholder
>
<Picker.Item label="Child" value={ProfileType.CHILD}/>
<Picker.Item label="Parent" value={ProfileType.PARENT}/>
<Picker.Item label="Caregiver" value={ProfileType.CAREGIVER}/>
<Picker.Item label="Family Device" value={ProfileType.FAMILY_DEVICE}/>
</Picker>
<Text style={styles.label}>
{selectedStatus === ProfileType.FAMILY_DEVICE ? "Device Name" : "First Name"}
</Text>
<TextField
editable={!isLoading}
placeholder={selectedStatus === ProfileType.FAMILY_DEVICE ? "Device name" : "First name"}
value={firstName}
onChangeText={setFirstName}
style={styles.inputField}
/>
{selectedStatus !== ProfileType.FAMILY_DEVICE && (
<>
<Text style={styles.label}>Last Name</Text>
<TextField
editable={!isLoading}
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
placeholder={
selectedStatus === ProfileType.FAMILY_DEVICE
? "Device name"
: "First name"
}
value={firstName}
onChangeText={setFirstName}
style={styles.inputField}
onSubmitEditing={() => {
lNameRef.current?.focus()
}}
blurOnSubmit={false}
returnKeyType="next"
/>
</>
)}
{selectedStatus !== ProfileType.FAMILY_DEVICE && (
<>
<Text style={styles.label}>Email Address (Optional)</Text>
<TextField
editable={!isLoading}
placeholder="Email address"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
style={styles.inputField}
{selectedStatus !== ProfileType.FAMILY_DEVICE && (
<>
<Text style={styles.jakarta12}>Last Name</Text>
<TextField
ref={lNameRef}
editable={!isLoading}
placeholder="Last name"
value={lastName}
onChangeText={setLastName}
style={styles.inputField}
onSubmitEditing={() => {
emailRef.current?.focus()
}}
blurOnSubmit={false}
returnKeyType="next"
/>
</>
)}
{selectedStatus !== ProfileType.FAMILY_DEVICE && (
<>
<Text style={styles.jakarta12}>Email Address (Optional)</Text>
<TextField
ref={emailRef}
editable={!isLoading}
placeholder="Email address"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
style={styles.inputField}
returnKeyType="done"
/>
</>
)}
<Button
disabled={
!firstName ||
(selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName) ||
isLoading
}
label={isLoading ? "Adding..." : "Add group member"}
backgroundColor="#fd1775"
labelStyle={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 15,
marginLeft: 7,
}}
style={{marginTop: 20, backgroundColor: "#fd1775"}}
iconSource={() => <NavToDosIcon width={22} color={"white"}/>}
onPress={handleCreateSubUser}
/>
</>
)}
<Button
disabled={!firstName || (selectedStatus !== ProfileType.FAMILY_DEVICE && !lastName) || isLoading}
label={isLoading ? "Adding..." : "Add group member"}
backgroundColor="#FD1775"
style={{marginTop: 20}}
onPress={handleCreateSubUser}
/>
</Card>
</Card>
</KeyboardAwareScrollView>
</PreviousNextView>
</Dialog>
</View>
);
};
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",
@ -347,6 +469,11 @@ const styles = StyleSheet.create({
borderRadius: 15,
padding: 20,
},
bottomButton: {
position: "absolute",
bottom: 80,
width: "100%",
},
familyCard: {
marginBottom: 10,
borderRadius: 10,
@ -354,6 +481,9 @@ const styles = StyleSheet.create({
width: "100%",
},
inputField: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13,
color: "#565656",
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
@ -373,6 +503,26 @@ const styles = StyleSheet.create({
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,
@ -386,7 +536,22 @@ const styles = StyleSheet.create({
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;

View File

@ -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<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const [timeZone, setTimeZone] = useState<string>(profileData?.timeZone! ?? Localization.getCalendars()[0].timeZone);
const [lastName, setLastName] = useState<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const { mutateAsync: updateUserData } = useUpdateUserData();
return (
<ScrollView style={{ paddingBottom: 100, flex: 1 }}>
<View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<ImageBackground
style={styles.pfp}
source={require("../../../../assets/images/profile-picture.png")}
/>
const {mutateAsync: updateUserData} = useUpdateUserData();
const isFirstRender = useRef(true);
<Text style={styles.photoSet} color="#50be0c">
Change Photo
</Text>
<Text style={styles.photoSet}>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
await updateUserData({ newUserData: { firstName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
await updateUserData({ newUserData: { lastName: value } });
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.subTit}>Settings</Text>
<Text text80 marginT-20 marginB-7 style={styles.label}>
Time Zone
</Text>
<TextField text70 placeholder="Time Zone" style={styles.txtBox} />
</View>
</ScrollView>
);
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 (
<ScrollView style={{paddingBottom: 100, flex: 1}}>
<View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<ImageBackground
style={styles.pfp}
source={require("../../../../assets/images/profile-picture.png")}
/>
<Text style={styles.photoSet} color="#50be0c">
Change Photo
</Text>
<Text style={styles.photoSet}>Remove Photo</Text>
</View>
<View paddingH-15>
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
editable={false}
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.subTit}>Settings</Text>
<Text style={styles.jakarta12}>Time Zone</Text>
<View style={styles.viewPicker}>
<Picker
// editable={!isLoading}
value={timeZone}
onChange={(item) => {
setTimeZone(item as string)
}}
showSearch
floatingPlaceholder
style={styles.inViewPicker}
trailingAccessory={
<View style={{
justifyContent: "center",
alignItems: "center",
height: "100%",
marginTop: -38,
paddingRight: 15
}}>
<Ionicons name={"chevron-down"} style={{alignSelf: "center"}} size={20}
color={"#000000"}/>
</View>
}
>
{timeZoneItems}
</Picker>
</View>
</View>
</ScrollView>
);
};
const timeZoneItems = Object.keys(tz.zones).sort().map((zone) => (
<Picker.Item key={zone} label={zone.replace("/", " / ").replace("_", " ")} value={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;

View File

@ -30,7 +30,7 @@ const UserMenu = ({
customContent={
<View height={18}>
<ListItem onPress={handleShowQRCode}>
<Text>Show Login QR Code</Text>
<Text style={{fontFamily: "Manrope_500Medium"}}>Show Login QR Code</Text>
</ListItem>
</View>
}

View File

@ -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) => {
</View>
<TextField
placeholder="Add a To Do"
autoFocus
value={todo?.title}
onChangeText={(text) => {
setTodo((oldValue: IToDo) => ({ ...oldValue, title: text }));
@ -197,25 +199,8 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
label="Assign"
/>
</View>
<View row marginH-13 marginT-13>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
<View row marginL-27 marginT-0>
<AssigneesDisplay />
</View>
<View row centerV style={styles.rotateSwitch}>
<Text text80>Take Turns</Text>

View File

@ -1,23 +1,77 @@
import React from "react";
import { ImageBackground, StyleSheet } from "react-native";
import { View } from "react-native-ui-lib";
import {ImageBackground, StyleSheet} from "react-native";
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
import RemoveAssigneeBtn from "./RemoveAssigneeBtn";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
const AssigneesDisplay = () => {
return (
<View row marginH-13 marginT-13 gap-20>
<ImageBackground
source={require("../../assets/images/child-picture.png")}
style={{ aspectRatio: 1, width: 58.08, overflow: "hidden" }}
children={<RemoveAssigneeBtn />}
/>
<ImageBackground
source={require("../../assets/images/child1-picture.png")}
style={{ aspectRatio: 1, width: 58.08, overflow: "hidden" }}
children={<RemoveAssigneeBtn />}
/>
</View>
);
const AssigneesDisplay = ({selectedAttendees, setSlectedAttendees}: {
selectedAttendees: string[],
setSlectedAttendees: (value: React.SetStateAction<string[]>) => void
}) => {
const {data: members} = useGetFamilyMembers(true);
const selectedMembers = members?.filter((x) => selectedAttendees.includes(x?.uid!));
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`;
};
const removeAttendee = (uid: string) => {
setSlectedAttendees((prev) => prev.filter((x) => x !== uid));
}
return (
<View row marginH-13 marginT-13 gap-20>
{selectedMembers?.map((member) => (
<TouchableOpacity key={member.uid} style={styles.assigneeWrapper}
onPress={() => removeAttendee(member.uid!)}>
{member?.pfp ? (
<ImageBackground
source={{uri: member?.pfp}}
style={styles.image}
children={<RemoveAssigneeBtn/>}
/>
) : (
<View style={styles.initialsCircle}>
<Text style={styles.initialsText}>
{getInitials(member.firstName, member.lastName)}
</Text>
</View>
)}
<RemoveAssigneeBtn/>
</TouchableOpacity>
))}
{selectedAttendees.length === 0 && <Text>No attendees added</Text>}
</View>
);
};
export default AssigneesDisplay;
const styles = StyleSheet.create({
assigneeWrapper: {
position: 'relative',
width: 58.08,
aspectRatio: 1,
},
image: {
aspectRatio: 1,
width: '100%',
borderRadius: 100, // Makes the image circular
overflow: 'hidden',
},
initialsCircle: {
backgroundColor: '#ccc',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 100, // Circular shape
width: '100%',
height: '100%',
},
initialsText: {
color: '#fff',
fontSize: 24,
fontWeight: 'bold',
},
});
export default AssigneesDisplay;

View File

@ -99,6 +99,7 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
setUser(authUser);
if (authUser) {
await refreshProfileData(authUser);
const pushToken = await registerForPushNotificationsAsync();

View File

@ -1,194 +0,0 @@
// CalendarContext.tsx
import React, { createContext, useContext, useState, ReactNode } from "react";
// Define the CalendarEvent interface
export interface CalendarEvent {
id?: number | string; // Unique identifier for the event
user?: string;
title: string; // Event title or name
description?: string; // Optional description for the event
start: Date; // Start date and time of the event
end: Date; // End date and time of the event
location?: string; // Optional event location
allDay?: boolean; // Specifies if the event lasts all day
color?: string; // Optional color to represent the event
participants?: string[]; // Optional list of participants or attendees
private?: boolean;
}
// Define the context type
interface CalendarContextType {
events: CalendarEvent[];
familyEvents: CalendarEvent[];
addEvent: (event: CalendarEvent) => void; // Function to add an event
removeEvent: (id: number) => void; // Function to remove an event by ID
updateEvent: (changes: Partial<CalendarEvent>, id?: number) => void;
}
// Create the CalendarContext
const CalendarContext = createContext<CalendarContextType | undefined>(
undefined
);
// Create a provider component
export const CalendarProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [events, setEvents] = useState<CalendarEvent[]>([
{
id: 1,
title: "Team Meeting",
description: "Discuss project milestones and deadlines.",
start: new Date("2024-09-15T10:00:00"),
end: new Date("2024-09-15T11:00:00"),
location: "Office Conference Room",
allDay: false,
color: "#FF5733",
participants: ["Alice", "Bob", "Charlie"],
},
{
id: 2,
title: "Doctor's Appointment",
description: "Annual check-up with Dr. Smith.",
start: new Date("2024-09-20T14:30:00"),
end: new Date("2024-09-20T15:30:00"),
location: "Health Clinic",
allDay: false,
color: "#33FF57",
participants: ["You"],
},
{
id: 3,
title: "Birthday Party",
description: "Celebrating Sarah's 30th birthday.",
start: new Date("2024-09-25T18:00:00"),
end: new Date("2024-09-25T21:00:00"),
location: "Sarah's House",
allDay: false,
color: "#3357FF",
participants: ["You", "Sarah", "Tom", "Lily"],
},
{
id: 4,
title: "Project Deadline",
description: "Final submission for the project.",
start: new Date("2024-10-01T00:00:00"),
end: new Date("2024-10-01T23:59:00"),
location: "Online",
allDay: false,
color: "#FF33A1",
participants: ["You"],
},
{
id: 5,
title: "Halloween Costume Party",
description: "Join us for a spooky night of fun!",
start: new Date("2024-10-31T19:00:00"),
end: new Date("2024-10-31T23:00:00"),
location: "Downtown Club",
allDay: false,
color: "#FFB733",
participants: ["You", "Friends"],
},
]);
const [familyEvents, setFamilyEvents] = useState<CalendarEvent[]>([
{
id: 1,
user: "jakesId",
title: "Team Meeting",
description: "Discuss project milestones and deadlines.",
start: new Date("2024-09-10T10:00:00"),
end: new Date("2024-09-10T11:00:00"),
location: "Office Conference Room",
allDay: false,
color: "#FF5733",
participants: ["Alice", "Bob", "Charlie"],
},
{
id: 2,
user: "mikesId",
title: "Doctor's Appointment",
description: "Annual check-up with Dr. Smith.",
start: new Date("2024-09-21T14:30:00"),
end: new Date("2024-09-21T15:30:00"),
location: "Health Clinic",
allDay: false,
color: "#33FF57",
participants: ["You"],
},
{
id: 3,
user: "jakesId",
title: "Birthday Party",
description: "Celebrating Sarah's 30th birthday.",
start: new Date("2024-09-5T18:00:00"),
end: new Date("2024-09-5T21:00:00"),
location: "Sarah's House",
allDay: false,
color: "#3357FF",
participants: ["You", "Sarah", "Tom", "Lily"],
},
{
id: 4,
user: "davidsId",
title: "Project Deadline",
description: "Final submission for the project.",
start: new Date("2024-10-03T00:00:00"),
end: new Date("2024-10-03T23:59:00"),
location: "Online",
allDay: false,
color: "#FF33A1",
participants: ["You"],
},
{
id: 5,
user: "jakesId",
title: "Halloween Costume Party",
description: "Join us for a spooky night of fun!",
start: new Date("2024-10-02T19:00:00"),
end: new Date("2024-10-02T23:00:00"),
location: "Downtown Club",
allDay: false,
color: "#FFB733",
participants: ["You", "Friends"],
},
]);
// Function to add an event
const addEvent = (event: CalendarEvent) => {
event.id = events.length + 1;
setEvents((prevEvents) => [...prevEvents, event]);
};
// Function to remove an event by ID
const removeEvent = (id: number) => {
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id));
};
// Function to update an event
const updateEvent = ( changes: Partial<CalendarEvent>, id?: number) => {
setEvents((prevEvents) =>
prevEvents.map((event) =>
event.id === id ? { ...event, ...changes } : event
)
);
};
return (
<CalendarContext.Provider
value={{ events, addEvent, removeEvent, updateEvent, familyEvents }}
>
{children}
</CalendarContext.Provider>
);
};
// Custom hook to use the CalendarContext
export const useCalendarContext = () => {
const context = useContext(CalendarContext);
if (!context) {
throw new Error("useCalendar must be used within a CalendarProvider");
}
return context;
};

View File

@ -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;
}
}

View File

@ -10,5 +10,6 @@ export interface EventData {
surpriseEvent?: boolean,
notes?: string,
reminders?: string[]
id?: string,
id?: string | number,
attendees?: string[]
}

View File

@ -1,39 +1,46 @@
import { ProfileType } from "@/contexts/AuthContext";
import {ProfileType} from "@/contexts/AuthContext";
export interface User {
uid: string;
email: string | null;
uid: string;
email: string | null;
}
export interface UserProfile {
userType: ProfileType;
firstName: string;
lastName: string;
childrenIds?: string[];
birthday?: Date;
parentId?: string;
contact?: string;
email: string;
password: string;
familyId?: string;
uid?: string;
googleToken?: string;
microsoftToken?: string;
eventColor?: string
userType: ProfileType;
firstName: string;
lastName: string;
childrenIds?: string[];
birthday?: Date;
parentId?: string;
contact?: string;
email: string;
password: string;
familyId?: string;
uid?: string;
pfp?: string;
googleToken?: string | null;
microsoftToken?: string | null;
appleToken?: string | null;
eventColor?: string | null;
googleMail?: string | null;
outlookMail?: string | null;
appleMail?: string | null;
timeZone?: string | null;
firstDayOfWeek?: string | null;
}
export interface ParentProfile extends UserProfile {
userType: ProfileType.PARENT;
childrenIds: string[];
userType: ProfileType.PARENT;
childrenIds: string[];
}
export interface ChildProfile extends UserProfile {
userType: ProfileType.CHILD;
birthday: Date;
parentId: string;
userType: ProfileType.CHILD;
birthday: Date;
parentId: string;
}
export interface CaregiverProfile extends UserProfile {
userType: ProfileType.CAREGIVER;
contact: string;
userType: ProfileType.CAREGIVER;
contact: string;
}

View File

@ -1,7 +1,7 @@
import { useAuthContext } from "@/contexts/AuthContext";
import { useMutation, useQueryClient } from "react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore";
import { EventData } from "@/hooks/firebase/types/eventData";
import {EventData} from "@/hooks/firebase/types/eventData";
export const useCreateEvent = () => {
const {user: currentUser, profileData} = useAuthContext()
@ -11,7 +11,6 @@ export const useCreateEvent = () => {
mutationKey: ["createEvent"],
mutationFn: async (eventData: Partial<EventData>) => {
try {
console.log("CALLLLL")
await firestore()
.collection("Events")
.add({...eventData, creatorId: currentUser?.uid, familyId: profileData?.familyId})
@ -25,37 +24,41 @@ export const useCreateEvent = () => {
})
}
export const useCreateEventFromProvider = () => {
const {user: currentUser} = useAuthContext()
const queryClients = useQueryClient()
export const useCreateEventsFromProvider = () => {
const {user: currentUser} = useAuthContext();
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createEventFromProvider"],
mutationFn: async (eventData: Partial<EventData>) => {
mutationKey: ["createEventsFromProvider"],
mutationFn: async (eventDataArray: Partial<EventData>[]) => {
try {
const snapshot = await firestore()
.collection("Events")
.where("id", "==", eventData.id)
.get();
for (const eventData of eventDataArray) {
console.log("Processing EventData: ", eventData);
if (snapshot.empty) {
await firestore()
const snapshot = await firestore()
.collection("Events")
.add({...eventData, creatorId: currentUser?.uid})
} else {
console.log("ENTER HERE")
const docId = snapshot.docs[0].id;
await firestore()
.collection("Events")
.doc(docId)
.update({...eventData, creatorId: currentUser?.uid});
.where("id", "==", eventData.id)
.get();
if (snapshot.empty) {
await firestore()
.collection("Events")
.add({...eventData, creatorId: currentUser?.uid});
} else {
console.log("Event already exists, updating...");
const docId = snapshot.docs[0].id;
await firestore()
.collection("Events")
.doc(docId)
.set({...eventData, creatorId: currentUser?.uid}, {merge: true});
}
}
} catch (e) {
console.error(e)
console.error("Error creating/updating events: ", e);
}
},
onSuccess: () => {
queryClients.invalidateQueries("events")
queryClient.invalidateQueries("events");
}
})
}
});
};

View File

@ -2,43 +2,69 @@ import {useQuery} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";
import {colorMap} from "@/contexts/SettingsContext";
import {useAtomValue} from "jotai";
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
export const useGetEvents = (isFamilyView: boolean) => {
const { user, profileData } = useAuthContext();
export const useGetEvents = () => {
const {user, profileData} = useAuthContext();
const isFamilyView = useAtomValue(isFamilyViewAtom)
return useQuery({
queryKey: ["events", user?.uid, isFamilyView],
queryFn: async () => {
const eventsQuery = firestore()
.collection("Events")
.where("creatorId", "==", user?.uid);
const db = firestore();
const userId = user?.uid; // Assuming user is defined
const familyId = profileData?.familyId; // Assuming profileData is defined
let allEvents = [];
if (isFamilyView) {
eventsQuery.where("familyID", "==", profileData?.familyId);
const familyQuery = db.collection("Events").where("familyID", "==", familyId);
const familySnapshot = await familyQuery.get();
const familyEvents = familySnapshot.docs.map(doc => doc.data());
allEvents = [...familyEvents];
} else {
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
const [creatorSnapshot, attendeeSnapshot] = await Promise.all([
creatorQuery.get(),
attendeeQuery.get(),
]);
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
allEvents = [...creatorEvents, ...attendeeEvents];
}
const snapshot = await eventsQuery.get();
allEvents = allEvents.filter((event, index, self) =>
index === self.findIndex(e => e.id === event.id)
);
return await Promise.all(snapshot.docs.map(async (doc) => {
const data = doc.data();
return await Promise.all(
allEvents.map(async (event) => {
const profileSnapshot = await db
.collection("Profiles")
.doc(event.creatorId)
.get();
const profileSnapshot = await firestore()
.collection("Profiles")
.doc(data.creatorId)
.get();
const profileData = profileSnapshot.data();
const eventColor = profileData?.eventColor || colorMap.pink; // Default color if not found
const profileData = profileSnapshot.data();
const eventColor: string = profileData?.eventColor || colorMap.pink // Default color if not found
return {
id: doc.id,
title: data.title,
start: new Date(data.startDate.seconds * 1000),
end: new Date(data.endDate.seconds * 1000),
hideHours: data.allDay,
eventColor: eventColor,
};
}));
return {
id: event.id,
title: event.title,
start: new Date(event.startDate.seconds * 1000),
end: new Date(event.endDate.seconds * 1000),
hideHours: event.allDay,
eventColor: eventColor,
};
})
);
},
staleTime: Infinity,
cacheTime: Infinity
});
};

View File

@ -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,
});

View File

@ -1,10 +1,8 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore";
import {EventData} from "@/hooks/firebase/types/eventData";
export const useUpdateEvent = () => {
const {user: currentUser} = useAuthContext()
const queryClients = useQueryClient()
return useMutation({
@ -13,7 +11,7 @@ export const useUpdateEvent = () => {
try {
await firestore()
.collection("Events")
.doc(eventData.id)
.doc(`${eventData.id}`)
.update(eventData);
} catch (e) {
console.error(e)

View File

@ -1,33 +1,46 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore";
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 queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updateUserData"],
mutationFn: async ({newUserData, customUser}: {newUserData: Partial<UserProfile>, customUser?: FirebaseAuthTypes.User }) => {
console.log("Mutation function called with data:", { newUserData, customUser });
mutationFn: async ({
newUserData,
customUser,
}: {
newUserData: Partial<UserProfile>;
customUser?: FirebaseAuthTypes.User;
}) => {
console.log("Mutation function called with data:", {newUserData, customUser});
const user = currentUser ?? customUser;
if (user) {
console.log("Updating user data for UID:", user.uid);
try {
console.log("New user data:", newUserData);
const updatedUserData = Object.fromEntries(
Object.entries(newUserData).map(([key, value]) =>
[key, value === null ? firestore.FieldValue.delete() : value]
)
);
console.log("Updated user data with deletions:", updatedUserData);
await firestore()
.collection("Profiles")
.doc(user.uid)
.update(newUserData);
.update(updatedUserData);
console.log("User data updated successfully, fetching updated profile...");
await refreshProfileData()
await refreshProfileData();
console.log("Profile data updated in context.");
} catch (e) {
@ -38,7 +51,7 @@ export const useUpdateUserData = () => {
}
},
onSuccess: () => {
queryClient.invalidateQueries("events")
}
queryClient.invalidateQueries("events");
},
});
};
};

View File

@ -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;
}
},
});
};

View File

@ -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
}
},
});
};

View File

@ -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;
}
},
});
};

View File

@ -14,6 +14,7 @@ install! 'cocoapods',
prepare_react_native_project!
target 'cally' do
pod 'IQKeyboardManagerSwift', :git => 'https://github.com/douglasjunior/IQKeyboardManager.git', :branch => 'react-native-keyboard-manager'
use_expo_modules!
config = use_native_modules!

View File

@ -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
@ -1489,6 +1495,7 @@ PODS:
- hermes-engine (0.74.3):
- hermes-engine/Pre-built (= 0.74.3)
- hermes-engine/Pre-built (0.74.3)
- IQKeyboardManagerSwift (6.5.16)
- leveldb-library (1.22.5)
- nanopb (2.30909.1):
- nanopb/decode (= 2.30909.1)
@ -2682,6 +2689,10 @@ PODS:
- React-logger (= 0.74.3)
- React-perflogger (= 0.74.3)
- React-utils (= 0.74.3)
- ReactNativeKeyboardManager (6.5.16-0):
- IQKeyboardManagerSwift (= 6.5.16)
- React-Core
- React-RCTText
- ReactNativeUiLib (4.2.0):
- React
- RecaptchaInterop (100.0.0)
@ -2802,7 +2813,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 +2824,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`)
@ -2822,6 +2836,7 @@ DEPENDENCIES:
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- IQKeyboardManagerSwift (from `https://github.com/douglasjunior/IQKeyboardManager.git`, branch `react-native-keyboard-manager`)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
@ -2874,6 +2889,7 @@ DEPENDENCIES:
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- ReactNativeKeyboardManager (from `../node_modules/react-native-keyboard-manager`)
- ReactNativeUiLib (from `../node_modules/react-native-ui-lib`)
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
@ -2956,8 +2972,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 +2994,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:
@ -2997,6 +3019,9 @@ EXTERNAL SOURCES:
hermes-engine:
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
:tag: hermes-2024-06-28-RNv0.74.3-7bda0c267e76d11b68a585f84cfdd65000babf85
IQKeyboardManagerSwift:
:branch: react-native-keyboard-manager
:git: https://github.com/douglasjunior/IQKeyboardManager.git
RCT-Folly:
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
RCTDeprecation:
@ -3097,6 +3122,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/utils"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
ReactNativeKeyboardManager:
:path: "../node_modules/react-native-keyboard-manager"
ReactNativeUiLib:
:path: "../node_modules/react-native-ui-lib"
RNDateTimePicker:
@ -3122,6 +3149,11 @@ EXTERNAL SOURCES:
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
CHECKOUT OPTIONS:
IQKeyboardManagerSwift:
:commit: 718cbed77cdd5ecd8b779afe543ba5b2df45b40a
:git: https://github.com/douglasjunior/IQKeyboardManager.git
SPEC CHECKSUMS:
abseil: d121da9ef7e2ff4cab7666e76c5a3e0915ae08c3
AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
@ -3142,7 +3174,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 +3185,7 @@ SPEC CHECKSUMS:
ExpoHead: fcb28a68ed4ba28f177394d2dfb8a0a8824cd103
ExpoImagePicker: 12a420923383ae38dccb069847218f27a3b87816
ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08
ExpoLocalization: f04eeec2e35bed01ab61c72ee1768ec04d093d01
ExpoModulesCore: db3e31e694684f08223d713e89f7648c6d3e04d0
ExpoSystemUI: d4f065a016cae6721b324eb659cdee4d4cf0cb26
ExpoWebBrowser: 7595ccac6938eb65b076385fd23d035db9ecdc8e
@ -3183,6 +3218,7 @@ SPEC CHECKSUMS:
gRPC-Core: eee4be35df218649fe66d721a05a7f27a28f069b
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
hermes-engine: 1f547997900dd0752dc0cc0ae6dd16173c49e09b
IQKeyboardManagerSwift: 90ba81812fbbd6694924a95a271fa3affdf04a14
leveldb-library: e8eadf9008a61f9e1dde3978c086d2b6d9b9dc28
nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
@ -3238,6 +3274,7 @@ SPEC CHECKSUMS:
React-runtimescheduler: e4ad653e1d2f5ff40ba047446cacde009694f0ed
React-utils: 6f7ac39d9a0de447d4334bb25d144a28c0c5d8c9
ReactCommon: 4a09c7d8a06e93c1e2e988a3b9f3db3d2449f2fc
ReactNativeKeyboardManager: 704d89bde3cb1e0f432bc273a44eec96eab9d90f
ReactNativeUiLib: deb877cd9b36cf5cad3c72b226bb330060681351
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
@ -3255,6 +3292,6 @@ SPEC CHECKSUMS:
Yoga: bd92064a0d558be92786820514d74fc4dddd1233
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 50f618790da7cbbfd5c5e988b7f9370bd45d34a6
PODFILE CHECKSUM: ae388457578eb44dbbdba1451a584b59f3bc21dd
COCOAPODS: 1.15.2

View File

@ -0,0 +1,7 @@
//
// ReactNativeKeyboardManager.swift
// cally
//
// Created by Milan Paunovic on 20.10.24..
//

View File

@ -21,7 +21,7 @@
/* Begin PBXFileReference section */
103D20271F044483964A389F /* cally-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "cally-Bridging-Header.h"; path = "cally/cally-Bridging-Header.h"; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* cally.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cally.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07F961A680F5B00A75B9A /* Cally.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cally.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = cally/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = cally/AppDelegate.mm; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = cally/Images.xcassets; sourceTree = "<group>"; };
@ -35,6 +35,7 @@
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F20F68FCCB33056D70B2396B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = cally/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
F3A90F152CC474F700DDA353 /* ReactNativeKeyboardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactNativeKeyboardManager.swift; sourceTree = "<group>"; };
F56C9EADA6FA4AEAA71245EB /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "cally/GoogleService-Info.plist"; sourceTree = "<group>"; };
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-cally/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -88,6 +89,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
F3A90F152CC474F700DDA353 /* ReactNativeKeyboardManager.swift */,
13B07FAE1A68108700A75B9A /* cally */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
@ -103,7 +105,7 @@
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* cally.app */,
13B07F961A680F5B00A75B9A /* Cally.app */,
);
name = Products;
sourceTree = "<group>";
@ -166,7 +168,7 @@
);
name = cally;
productName = cally;
productReference = 13B07F961A680F5B00A75B9A /* cally.app */;
productReference = 13B07F961A680F5B00A75B9A /* Cally.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@ -178,7 +180,9 @@
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = MV9C3PHV87;
LastSwiftMigration = 1250;
ProvisioningStyle = Automatic;
};
};
};
@ -302,6 +306,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",
@ -314,6 +319,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle",
@ -337,6 +343,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",
@ -349,6 +356,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IQKeyboardManagerSwift.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle",
@ -421,7 +429,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 +452,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 +468,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 +486,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";

View File

@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "cally.app"
BuildableName = "Cally.app"
BlueprintName = "cally"
ReferencedContainer = "container:cally.xcodeproj">
</BuildableReference>
@ -55,7 +55,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "cally.app"
BuildableName = "Cally.app"
BlueprintName = "cally"
ReferencedContainer = "container:cally.xcodeproj">
</BuildableReference>
@ -72,7 +72,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "cally.app"
BuildableName = "Cally.app"
BlueprintName = "cally"
ReferencedContainer = "container:cally.xcodeproj">
</BuildableReference>

View File

@ -4,10 +4,12 @@
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Cally - Family Planner</string>
<string>Cally.</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -45,7 +47,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>28</string>
<string>31</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
@ -55,12 +57,20 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>The app needs to access your calendar.</string>
<key>NSCalendarsUsageDescription</key>
<string>The app needs to access your calendar.</string>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your photos</string>
<key>NSRemindersFullAccessUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your reminders</string>
<key>NSRemindersUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your reminders</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
@ -95,6 +105,12 @@
<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>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>

View File

@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>

View File

@ -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",
@ -73,6 +76,7 @@
"react-native-calendars": "^1.1306.0",
"react-native-gesture-handler": "~2.16.1",
"react-native-gifted-charts": "^1.4.41",
"react-native-keyboard-manager": "^6.5.16-0",
"react-native-linear-gradient": "^2.8.3",
"react-native-onboarding-swiper": "^1.3.0",
"react-native-qrcode-svg": "^6.3.2",
@ -84,7 +88,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",

42
plugins/withPodfile.js Normal file
View File

@ -0,0 +1,42 @@
const { withDangerousMod, withPlugins } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');
async function readFile(path) {
return fs.promises.readFile(path, 'utf8');
}
async function saveFile(path, content) {
return fs.promises.writeFile(path, content, 'utf8');
}
module.exports = (config) =>
withPlugins(config, [
(config) => {
return withDangerousMod(config, [
'iOS',
async (config) => {
const file = path.join(config.modRequest.platformProjectRoot, 'Podfile');
/*
* You need to remove the line before adding it.
* If you don't do this and you run `expo prebuild` in a dirt project
* your file will have the same line added twice
*/
const contents = (await readFile(file)).replace(
/pod 'IQKeyboardManagerSwift', :git => 'https:\/\/github.com\/douglasjunior\/IQKeyboardManager.git', :branch => 'react-native-keyboard-manager'\n\n/g,
'',
);
/*
* Now re-adds the content
*/
await saveFile(
file,
`pod 'IQKeyboardManagerSwift', :git => 'https://github.com/douglasjunior/IQKeyboardManager.git', :branch => 'react-native-keyboard-manager'\n\n${contents}`,
);
return config;
},
]);
},
]);

1020
yarn.lock

File diff suppressed because it is too large Load Diff