Merge remote-tracking branch 'origin/dev' into dev
85
.idea/jsLinters/jshint.xml
generated
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JSHintConfiguration" version="2.13.6" use-config-file="false">
|
||||||
|
<option asi="false" />
|
||||||
|
<option bitwise="true" />
|
||||||
|
<option boss="false" />
|
||||||
|
<option browser="true" />
|
||||||
|
<option browserify="false" />
|
||||||
|
<option camelcase="true" />
|
||||||
|
<option couch="false" />
|
||||||
|
<option curly="true" />
|
||||||
|
<option debug="false" />
|
||||||
|
<option devel="false" />
|
||||||
|
<option dojo="false" />
|
||||||
|
<option elision="false" />
|
||||||
|
<option enforceall="false" />
|
||||||
|
<option eqeqeq="true" />
|
||||||
|
<option eqnull="false" />
|
||||||
|
<option es3="false" />
|
||||||
|
<option es5="false" />
|
||||||
|
<option esnext="false" />
|
||||||
|
<option evil="false" />
|
||||||
|
<option expr="false" />
|
||||||
|
<option forin="true" />
|
||||||
|
<option freeze="true" />
|
||||||
|
<option funcscope="false" />
|
||||||
|
<option futurehostile="false" />
|
||||||
|
<option gcl="false" />
|
||||||
|
<option globalstrict="false" />
|
||||||
|
<option immed="false" />
|
||||||
|
<option iterator="false" />
|
||||||
|
<option jasmine="false" />
|
||||||
|
<option jquery="false" />
|
||||||
|
<option lastsemic="false" />
|
||||||
|
<option latedef="false" />
|
||||||
|
<option laxbreak="false" />
|
||||||
|
<option laxcomma="false" />
|
||||||
|
<option loopfunc="false" />
|
||||||
|
<option maxerr="50" />
|
||||||
|
<option mocha="false" />
|
||||||
|
<option module="false" />
|
||||||
|
<option mootools="false" />
|
||||||
|
<option moz="false" />
|
||||||
|
<option multistr="false" />
|
||||||
|
<option newcap="false" />
|
||||||
|
<option noarg="true" />
|
||||||
|
<option nocomma="false" />
|
||||||
|
<option node="false" />
|
||||||
|
<option noempty="true" />
|
||||||
|
<option nomen="false" />
|
||||||
|
<option nonbsp="false" />
|
||||||
|
<option nonew="true" />
|
||||||
|
<option nonstandard="false" />
|
||||||
|
<option notypeof="false" />
|
||||||
|
<option noyield="false" />
|
||||||
|
<option onevar="false" />
|
||||||
|
<option passfail="false" />
|
||||||
|
<option phantom="false" />
|
||||||
|
<option plusplus="false" />
|
||||||
|
<option proto="false" />
|
||||||
|
<option prototypejs="false" />
|
||||||
|
<option qunit="false" />
|
||||||
|
<option quotmark="false" />
|
||||||
|
<option rhino="false" />
|
||||||
|
<option scripturl="false" />
|
||||||
|
<option shadow="false" />
|
||||||
|
<option shelljs="false" />
|
||||||
|
<option singleGroups="false" />
|
||||||
|
<option smarttabs="false" />
|
||||||
|
<option strict="true" />
|
||||||
|
<option sub="false" />
|
||||||
|
<option supernew="false" />
|
||||||
|
<option trailing="false" />
|
||||||
|
<option typed="false" />
|
||||||
|
<option undef="true" />
|
||||||
|
<option unused="false" />
|
||||||
|
<option validthis="false" />
|
||||||
|
<option varstmt="false" />
|
||||||
|
<option white="false" />
|
||||||
|
<option withstmt="false" />
|
||||||
|
<option worker="false" />
|
||||||
|
<option wsh="false" />
|
||||||
|
<option yui="false" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -1,4 +1,5 @@
|
|||||||
package com.cally.app
|
package com.cally.app
|
||||||
|
import expo.modules.splashscreen.SplashScreenManager
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -15,7 +16,10 @@ class MainActivity : ReactActivity() {
|
|||||||
// Set the theme to AppTheme BEFORE onCreate to support
|
// Set the theme to AppTheme BEFORE onCreate to support
|
||||||
// coloring the background, status bar, and navigation bar.
|
// coloring the background, status bar, and navigation bar.
|
||||||
// This is required for expo-splash-screen.
|
// This is required for expo-splash-screen.
|
||||||
setTheme(R.style.AppTheme);
|
// setTheme(R.style.AppTheme);
|
||||||
|
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||||
|
SplashScreenManager.registerOnActivity(this)
|
||||||
|
// @generated end expo-splashscreen
|
||||||
super.onCreate(null)
|
super.onCreate(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 29 KiB |
@ -0,0 +1,6 @@
|
|||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@color/splashscreen_background"/>
|
||||||
|
<item>
|
||||||
|
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 45 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 97 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 163 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 21 KiB |
@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">\"Cally \"</string>
|
<string name="app_name">"Cally "</string>
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</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_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
<item name="android:textColorHint">#c8c8c8</item>
|
<item name="android:textColorHint">#c8c8c8</item>
|
||||||
<item name="android:textColor">@android:color/black</item>
|
<item name="android:textColor">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||||
|
<item name="android:windowBackground">@drawable/splashscreen_logo</item>
|
||||||
|
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
3
app.json
@ -4,6 +4,7 @@
|
|||||||
"slug": "cally",
|
"slug": "cally",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
|
"owner": "tomira",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "callyplanner",
|
"scheme": "callyplanner",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
@ -16,7 +17,7 @@
|
|||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "com.cally.app",
|
"bundleIdentifier": "com.cally.app",
|
||||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||||
"buildNumber": "74",
|
"buildNumber": "100",
|
||||||
"usesAppleSignIn": true,
|
"usesAppleSignIn": true,
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"ITSAppUsesNonExemptEncryption": false
|
"ITSAppUsesNonExemptEncryption": false
|
||||||
|
@ -36,6 +36,7 @@ import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
|||||||
import { RouteProp } from "@react-navigation/core";
|
import { RouteProp } from "@react-navigation/core";
|
||||||
import RefreshButton from "@/components/shared/RefreshButton";
|
import RefreshButton from "@/components/shared/RefreshButton";
|
||||||
import { useCalSync } from "@/hooks/useCalSync";
|
import { useCalSync } from "@/hooks/useCalSync";
|
||||||
|
import {useIsFetching} from "@tanstack/react-query";
|
||||||
|
|
||||||
type DrawerParamList = {
|
type DrawerParamList = {
|
||||||
index: undefined;
|
index: undefined;
|
||||||
@ -77,6 +78,11 @@ export default function TabLayout() {
|
|||||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||||
const { resyncAllCalendars, isSyncing } = useCalSync();
|
const { resyncAllCalendars, isSyncing } = useCalSync();
|
||||||
|
|
||||||
|
const isFormatting = useIsFetching({queryKey: ['formattedEvents']}) > 0;
|
||||||
|
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
|
||||||
|
|
||||||
|
const isLoading = isSyncing || isFormatting || isFetching;
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
const onRefresh = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await resyncAllCalendars();
|
await resyncAllCalendars();
|
||||||
@ -116,7 +122,7 @@ export default function TabLayout() {
|
|||||||
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||||
return isCalendarPage ? (
|
return isCalendarPage ? (
|
||||||
<View marginR-16>
|
<View marginR-16>
|
||||||
<RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} />
|
<RefreshButton onRefresh={onRefresh} isSyncing={isLoading} />
|
||||||
</View>
|
</View>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
@ -124,7 +130,7 @@ export default function TabLayout() {
|
|||||||
return (
|
return (
|
||||||
<View marginR-16 row centerV>
|
<View marginR-16 row centerV>
|
||||||
{isCalendarPage && (
|
{isCalendarPage && (
|
||||||
<View marginR-16><RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} /></View>
|
<View marginR-16><RefreshButton onRefresh={onRefresh} isSyncing={isLoading} /></View>
|
||||||
)}
|
)}
|
||||||
<MemoizedViewSwitch navigation={navigation} />
|
<MemoizedViewSwitch navigation={navigation} />
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, {useEffect} from "react";
|
||||||
import {RefreshControl, ScrollView, View} from "react-native";
|
import {RefreshControl, ScrollView, View} from "react-native";
|
||||||
import CalendarPage from "@/components/pages/calendar/CalendarPage";
|
import CalendarPage from "@/components/pages/calendar/CalendarPage";
|
||||||
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
|
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
|
||||||
@ -6,9 +6,9 @@ import * as Device from "expo-device";
|
|||||||
import {DeviceType} from "expo-device";
|
import {DeviceType} from "expo-device";
|
||||||
import {useCalSync} from "@/hooks/useCalSync";
|
import {useCalSync} from "@/hooks/useCalSync";
|
||||||
import {colorMap} from "@/constants/colorMap";
|
import {colorMap} from "@/constants/colorMap";
|
||||||
import { useSetAtom } from "jotai";
|
import {useSetAtom} from "jotai";
|
||||||
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
|
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
const isTablet = Device.deviceType === DeviceType.TABLET;
|
const isTablet = Device.deviceType === DeviceType.TABLET;
|
||||||
@ -17,9 +17,14 @@ export default function Screen() {
|
|||||||
const {profileData} = useAuthContext()
|
const {profileData} = useAuthContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!isTablet && profileData) setSelectedUser({firstName: profileData.firstName, lastName: profileData.lastName, eventColor: profileData.eventColor})
|
if (!isTablet && profileData) setSelectedUser({
|
||||||
|
uid: profileData.uid!,
|
||||||
|
firstName: profileData.firstName,
|
||||||
|
lastName: profileData.lastName,
|
||||||
|
eventColor: profileData?.eventColor!
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
const onRefresh = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -80,15 +85,8 @@ export default function Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<View style={{flex: 1}}>
|
||||||
style={{flex: 1, height: "100%"}}
|
<CalendarPage/>
|
||||||
contentContainerStyle={{flex: 1, height: "100%"}}
|
</View>
|
||||||
bounces={true}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
>
|
|
||||||
<View style={{flex: 1}}>
|
|
||||||
<CalendarPage/>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -50,19 +50,17 @@ import {Stack} from "expo-router";
|
|||||||
import * as SplashScreen from "expo-splash-screen";
|
import * as SplashScreen from "expo-splash-screen";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import {AuthContextProvider} from "@/contexts/AuthContext";
|
import {AuthContextProvider} from "@/contexts/AuthContext";
|
||||||
import {QueryClient, QueryClientProvider} from "react-query";
|
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||||
import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
|
import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
|
||||||
import {Platform} from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import KeyboardManager from 'react-native-keyboard-manager';
|
import KeyboardManager from 'react-native-keyboard-manager';
|
||||||
import {enableScreens} from 'react-native-screens';
|
import {enableScreens} from 'react-native-screens';
|
||||||
|
import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider";
|
||||||
|
|
||||||
enableScreens(true)
|
enableScreens(true)
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
KeyboardManager.setEnable(true);
|
KeyboardManager.setEnable(true);
|
||||||
KeyboardManager.setToolbarPreviousNextButtonEnable(true);
|
KeyboardManager.setToolbarPreviousNextButtonEnable(true);
|
||||||
@ -211,8 +209,6 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
SplashScreen.hideAsync();
|
|
||||||
|
|
||||||
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
|
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
@ -262,7 +258,7 @@ export default function RootLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<PersistQueryClientProvider>
|
||||||
<AuthContextProvider>
|
<AuthContextProvider>
|
||||||
<ThemeProvider value={DefaultTheme}>
|
<ThemeProvider value={DefaultTheme}>
|
||||||
<Stack>
|
<Stack>
|
||||||
@ -273,6 +269,6 @@ export default function RootLayout() {
|
|||||||
<Toast/>
|
<Toast/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AuthContextProvider>
|
</AuthContextProvider>
|
||||||
</QueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
module.exports = function (api) {
|
module.exports = function (api) {
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
|
|
||||||
|
let plugins = [];
|
||||||
|
|
||||||
|
if (env !== 'development') {
|
||||||
|
plugins.push('transform-remove-console');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
presets: [
|
presets: [
|
||||||
'babel-preset-expo',
|
'babel-preset-expo',
|
||||||
]
|
],
|
||||||
|
plugins
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -10,6 +10,7 @@ import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
|||||||
import {useSetAtom} from "jotai";
|
import {useSetAtom} from "jotai";
|
||||||
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
||||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
import {addMinutes, roundToNearestMinutes} from "date-fns";
|
||||||
|
|
||||||
export const AddEventDialog = () => {
|
export const AddEventDialog = () => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
@ -20,7 +21,8 @@ export const AddEventDialog = () => {
|
|||||||
const handleOpenManualInputModal = () => {
|
const handleOpenManualInputModal = () => {
|
||||||
setShow(false);
|
setShow(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSelectedNewEndDate(new Date());
|
const roundedDate = roundToNearestMinutes(new Date(), {nearestTo: 5});
|
||||||
|
setSelectedNewEndDate(roundedDate);
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ export const AddEventDialog = () => {
|
|||||||
onPress={() => setShow(true)}
|
onPress={() => setShow(true)}
|
||||||
>
|
>
|
||||||
<View row centerV centerH>
|
<View row centerV centerH>
|
||||||
<PlusIcon />
|
<PlusIcon/>
|
||||||
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
|
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
|
||||||
New
|
New
|
||||||
</Text>
|
</Text>
|
||||||
|
103
components/pages/calendar/DetailedCalendar.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React, {useCallback, useEffect, useMemo, useRef} from "react";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {useAtomValue} from "jotai";
|
||||||
|
import {modeAtom, selectedDateAtom, selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||||
|
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
|
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit";
|
||||||
|
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||||
|
import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents";
|
||||||
|
import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls";
|
||||||
|
import {EventCell} from "@/components/pages/calendar/EventCell";
|
||||||
|
import {isToday} from "date-fns";
|
||||||
|
|
||||||
|
interface EventCalendarProps {
|
||||||
|
calendarHeight: number;
|
||||||
|
calendarWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DetailedCalendar: React.FC<EventCalendarProps> = ({calendarHeight, calendarWidth}) => {
|
||||||
|
const {profileData} = useAuthContext();
|
||||||
|
const selectedDate = useAtomValue(selectedDateAtom);
|
||||||
|
const mode = useAtomValue(modeAtom);
|
||||||
|
const {data: familyMembers} = useGetFamilyMembers();
|
||||||
|
const calendarRef = useRef<CalendarKitHandle>(null);
|
||||||
|
const {data: events} = useGetEvents();
|
||||||
|
const selectedUser = useAtomValue(selectedUserAtom);
|
||||||
|
|
||||||
|
const {data: formattedEvents} = useFormattedEvents(events ?? [], selectedDate, selectedUser);
|
||||||
|
const {
|
||||||
|
handlePressEvent,
|
||||||
|
handlePressCell,
|
||||||
|
debouncedOnDateChanged
|
||||||
|
} = useCalendarControls(events ?? []);
|
||||||
|
|
||||||
|
const numberOfDays = useMemo(() => {
|
||||||
|
return mode === 'week' ? 7 : mode === '3days' ? 3 : 1;
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
const firstDay = useMemo(() => {
|
||||||
|
return profileData?.firstDayOfWeek === "Mondays" ? 1 : 0;
|
||||||
|
}, [profileData?.firstDayOfWeek]);
|
||||||
|
|
||||||
|
const headerProps = useMemo(() => ({
|
||||||
|
dayBarHeight: 60,
|
||||||
|
headerBottomHeight: 20
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const bodyProps = useMemo(() => ({
|
||||||
|
showNowIndicator: true,
|
||||||
|
hourFormat: "h:mm a"
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const containerProps = useMemo(() => ({
|
||||||
|
hourWidth: 70,
|
||||||
|
allowPinchToZoom: true,
|
||||||
|
useHaptic: true,
|
||||||
|
scrollToNow: true,
|
||||||
|
initialDate: selectedDate.toISOString(),
|
||||||
|
}), [selectedDate]);
|
||||||
|
|
||||||
|
const renderEvent = useCallback((event: any) => {
|
||||||
|
const attendees = useMemo(() =>
|
||||||
|
familyMembers?.filter(member => event?.attendees?.includes(member?.uid!)) || [],
|
||||||
|
[familyMembers, event.attendees]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventCell
|
||||||
|
event={event}
|
||||||
|
onPress={handlePressEvent}
|
||||||
|
attendees={attendees}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [familyMembers, handlePressEvent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedDate && isToday(selectedDate)) {
|
||||||
|
calendarRef?.current?.goToDate({date: selectedDate});
|
||||||
|
}
|
||||||
|
}, [selectedDate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CalendarContainer
|
||||||
|
ref={calendarRef}
|
||||||
|
{...containerProps}
|
||||||
|
numberOfDays={numberOfDays}
|
||||||
|
calendarWidth={calendarWidth}
|
||||||
|
|
||||||
|
onDateChanged={debouncedOnDateChanged}
|
||||||
|
firstDay={firstDay}
|
||||||
|
events={formattedEvents ?? []}
|
||||||
|
onPressEvent={handlePressEvent}
|
||||||
|
onPressBackground={handlePressCell}
|
||||||
|
>
|
||||||
|
<CalendarHeader {...headerProps} />
|
||||||
|
<CalendarBody
|
||||||
|
{...bodyProps}
|
||||||
|
renderEvent={renderEvent}
|
||||||
|
/>
|
||||||
|
</CalendarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailedCalendar;
|
@ -1,734 +1,52 @@
|
|||||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
import React from 'react';
|
||||||
import {Calendar} from "react-native-big-calendar";
|
import {StyleSheet, View, ActivityIndicator} from 'react-native';
|
||||||
import {ActivityIndicator, StyleSheet, TouchableOpacity, View, ViewStyle} from "react-native";
|
import {Text} from 'react-native-ui-lib';
|
||||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
|
||||||
import {useAtom, useSetAtom} from "jotai";
|
import {useCalSync} from '@/hooks/useCalSync';
|
||||||
|
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
|
||||||
|
import {useAtom} from 'jotai';
|
||||||
import {
|
import {
|
||||||
editVisibleAtom,
|
|
||||||
eventForEditAtom,
|
|
||||||
isAllDayAtom,
|
|
||||||
isFamilyViewAtom,
|
|
||||||
modeAtom,
|
modeAtom,
|
||||||
selectedDateAtom,
|
} from './atoms';
|
||||||
selectedNewEventDateAtom,
|
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
|
||||||
selectedUserAtom,
|
import {DetailedCalendar} from "@/components/pages/calendar/DetailedCalendar";
|
||||||
} from "@/components/pages/calendar/atoms";
|
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
|
||||||
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
|
||||||
import {Text} from "react-native-ui-lib";
|
|
||||||
import {addDays, compareAsc, format, isWithinInterval, subDays} from "date-fns";
|
|
||||||
import {useCalSync} from "@/hooks/useCalSync";
|
|
||||||
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
|
|
||||||
import {colorMap} from "@/constants/colorMap";
|
|
||||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
|
||||||
import CachedImage from "expo-cached-image";
|
|
||||||
import { DeviceType } from "expo-device";
|
|
||||||
import * as Device from "expo-device"
|
|
||||||
|
|
||||||
interface EventCalendarProps {
|
interface EventCalendarProps {
|
||||||
calendarHeight: number;
|
calendarHeight: number;
|
||||||
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
|
|
||||||
calendarWidth: number;
|
calendarWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTotalMinutes = () => {
|
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
|
||||||
const date = new Date();
|
const {data: events, isLoading} = useGetEvents();
|
||||||
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
|
const [mode] = useAtom(modeAtom);
|
||||||
};
|
const {isSyncing} = useSyncEvents();
|
||||||
|
useCalSync();
|
||||||
|
|
||||||
|
if (isLoading || isSyncing) {
|
||||||
const processEventsForSideBySide = (events: CalendarEvent[]) => {
|
|
||||||
if (!events) return [];
|
|
||||||
|
|
||||||
// Group events by day and time slot
|
|
||||||
const timeSlots: { [key: string]: CalendarEvent[] } = {};
|
|
||||||
|
|
||||||
events.forEach(event => {
|
|
||||||
const startDate = new Date(event.start);
|
|
||||||
const endDate = new Date(event.end);
|
|
||||||
|
|
||||||
// If it's an all-day event, mark it and add it directly
|
|
||||||
if (event.allDay) {
|
|
||||||
const key = `${startDate.toISOString().split('T')[0]}-allday`;
|
|
||||||
if (!timeSlots[key]) timeSlots[key] = [];
|
|
||||||
timeSlots[key].push({
|
|
||||||
...event,
|
|
||||||
isAllDayEvent: true,
|
|
||||||
width: 1,
|
|
||||||
xPos: 0
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle multi-day events
|
|
||||||
if (startDate.toDateString() !== endDate.toDateString()) {
|
|
||||||
// Create array of dates between start and end
|
|
||||||
const dates = [];
|
|
||||||
let currentDate = new Date(startDate);
|
|
||||||
|
|
||||||
while (currentDate <= endDate) {
|
|
||||||
dates.push(new Date(currentDate));
|
|
||||||
currentDate.setDate(currentDate.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create segments for each day
|
|
||||||
dates.forEach((date, index) => {
|
|
||||||
const isFirstDay = index === 0;
|
|
||||||
const isLastDay = index === dates.length - 1;
|
|
||||||
|
|
||||||
let segmentStart, segmentEnd;
|
|
||||||
|
|
||||||
if (isFirstDay) {
|
|
||||||
// First day: use original start time to end of day
|
|
||||||
segmentStart = new Date(startDate);
|
|
||||||
segmentEnd = new Date(date);
|
|
||||||
segmentEnd.setHours(23, 59, 59);
|
|
||||||
} else if (isLastDay) {
|
|
||||||
// Last day: use start of day to original end time
|
|
||||||
segmentStart = new Date(date);
|
|
||||||
segmentStart.setHours(0, 0, 0);
|
|
||||||
segmentEnd = new Date(endDate);
|
|
||||||
} else {
|
|
||||||
// Middle days: full day
|
|
||||||
segmentStart = new Date(date);
|
|
||||||
segmentStart.setHours(0, 0, 0);
|
|
||||||
segmentEnd = new Date(date);
|
|
||||||
segmentEnd.setHours(23, 59, 59);
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = `${segmentStart.toISOString().split('T')[0]}-${segmentStart.getHours()}`;
|
|
||||||
if (!timeSlots[key]) timeSlots[key] = [];
|
|
||||||
|
|
||||||
timeSlots[key].push({
|
|
||||||
...event,
|
|
||||||
start: segmentStart,
|
|
||||||
end: segmentEnd,
|
|
||||||
isMultiDaySegment: true,
|
|
||||||
isFirstDay,
|
|
||||||
isLastDay,
|
|
||||||
originalStart: startDate,
|
|
||||||
originalEnd: endDate,
|
|
||||||
allDay: true // Mark multi-day events as all-day events
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Regular event
|
|
||||||
const key = `${startDate.toISOString().split('T')[0]}-${startDate.getHours()}`;
|
|
||||||
if (!timeSlots[key]) timeSlots[key] = [];
|
|
||||||
timeSlots[key].push(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process all time slots
|
|
||||||
return Object.values(timeSlots).flatMap(slotEvents => {
|
|
||||||
// Sort events by start time
|
|
||||||
slotEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
||||||
|
|
||||||
// Find overlapping events (only for non-all-day events)
|
|
||||||
return slotEvents.map((event, index) => {
|
|
||||||
// If it's an all-day or multi-day event, return as is
|
|
||||||
if (event.allDay || event.isMultiDaySegment) {
|
|
||||||
return {
|
|
||||||
...event,
|
|
||||||
width: 1,
|
|
||||||
xPos: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle regular events
|
|
||||||
const overlappingEvents = slotEvents.filter((otherEvent, otherIndex) => {
|
|
||||||
if (index === otherIndex || otherEvent.allDay || otherEvent.isMultiDaySegment) return false;
|
|
||||||
const eventStart = new Date(event.start);
|
|
||||||
const eventEnd = new Date(event.end);
|
|
||||||
const otherStart = new Date(otherEvent.start);
|
|
||||||
const otherEnd = new Date(otherEvent.end);
|
|
||||||
return (eventStart < otherEnd && eventEnd > otherStart);
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = overlappingEvents.length + 1;
|
|
||||||
const position = index % total;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...event,
|
|
||||||
width: 1 / total,
|
|
||||||
xPos: position / total
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEvent = (event: CalendarEvent & {
|
|
||||||
width: number;
|
|
||||||
xPos: number;
|
|
||||||
isMultiDaySegment?: boolean;
|
|
||||||
isFirstDay?: boolean;
|
|
||||||
isLastDay?: boolean;
|
|
||||||
originalStart?: Date;
|
|
||||||
originalEnd?: Date;
|
|
||||||
isAllDayEvent?: boolean;
|
|
||||||
allDay?: boolean;
|
|
||||||
eventColor?: string;
|
|
||||||
attendees?: string[];
|
|
||||||
creatorId?: string;
|
|
||||||
pfp?: string;
|
|
||||||
firstName?: string;
|
|
||||||
lastName?: string;
|
|
||||||
notes?: string;
|
|
||||||
hideHours?: boolean;
|
|
||||||
}, props: any) => {
|
|
||||||
const {data: familyMembers} = useGetFamilyMembers();
|
|
||||||
|
|
||||||
const attendees = useMemo(() => {
|
|
||||||
if (!familyMembers || !event.attendees) return event?.creatorId ? [event?.creatorId] : [];
|
|
||||||
return familyMembers.filter(member => event?.attendees?.includes(member?.uid!));
|
|
||||||
}, [familyMembers, event.attendees]);
|
|
||||||
|
|
||||||
if (event.allDay && !!event.isMultiDaySegment) {
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<View style={styles.loadingContainer}>
|
||||||
{...props}
|
{isSyncing && <Text>Syncing...</Text>}
|
||||||
style={[
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
props.style,
|
|
||||||
{
|
|
||||||
width: '100%',
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={styles.allDayEventText} numberOfLines={1}>
|
|
||||||
{event.title}
|
|
||||||
{event.isMultiDaySegment &&
|
|
||||||
` (${format(new Date(event.start), 'MMM d')} - ${format(new Date(event.end), 'MMM d')})`
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalStyle = Array.isArray(props.style) ? props.style[0] : props.style;
|
|
||||||
|
|
||||||
console.log('Rendering event:', {
|
|
||||||
title: event.title,
|
|
||||||
start: event.start,
|
|
||||||
end: event.end,
|
|
||||||
width: event.width,
|
|
||||||
xPos: event.xPos,
|
|
||||||
isMultiDaySegment: event.isMultiDaySegment
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Ensure we have Date objects
|
|
||||||
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
|
|
||||||
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
|
|
||||||
|
|
||||||
const hourHeight = props.hourHeight || 60;
|
|
||||||
const startHour = startDate.getHours();
|
|
||||||
const startMinutes = startDate.getMinutes();
|
|
||||||
const topPosition = (startHour + startMinutes / 60) * hourHeight;
|
|
||||||
|
|
||||||
const endHour = endDate.getHours();
|
|
||||||
const endMinutes = endDate.getMinutes();
|
|
||||||
const duration = (endHour + endMinutes / 60) - (startHour + startMinutes / 60);
|
|
||||||
const height = duration * hourHeight;
|
|
||||||
|
|
||||||
const formatTime = (date: Date) => {
|
|
||||||
const hours = date.getHours();
|
|
||||||
const minutes = date.getMinutes();
|
|
||||||
const ampm = hours >= 12 ? 'pm' : 'am';
|
|
||||||
const formattedHours = hours % 12 || 12;
|
|
||||||
const formattedMinutes = minutes.toString().padStart(2, '0');
|
|
||||||
return `${formattedHours}:${formattedMinutes}${ampm}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeString = event.isMultiDaySegment
|
|
||||||
? event.isFirstDay
|
|
||||||
? `${formatTime(startDate)} → 12:00PM`
|
|
||||||
: event.isLastDay
|
|
||||||
? `12:00am → ${formatTime(endDate)}`
|
|
||||||
: 'All day'
|
|
||||||
: `${formatTime(startDate)} - ${formatTime(endDate)}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
{...props}
|
|
||||||
style={[
|
|
||||||
originalStyle,
|
|
||||||
{
|
|
||||||
position: 'absolute',
|
|
||||||
width: `${event.width * 100}%`,
|
|
||||||
left: `${event.xPos * 100}%`,
|
|
||||||
top: topPosition,
|
|
||||||
height: height,
|
|
||||||
zIndex: event.isMultiDaySegment ? 1 : 2,
|
|
||||||
shadowRadius: 2,
|
|
||||||
overflow: "hidden"
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: event.eventColor,
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: 8,
|
|
||||||
justifyContent: 'space-between'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: 'white',
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
|
||||||
fontWeight: '600',
|
|
||||||
marginBottom: 4
|
|
||||||
}}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{event.title}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: 'white',
|
|
||||||
fontSize: 10,
|
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
|
||||||
opacity: 0.8
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{timeString}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Attendees Section */}
|
|
||||||
{attendees?.length > 0 && (
|
|
||||||
<View style={{flexDirection: 'row', marginTop: 8, height: 27.32}}>
|
|
||||||
{attendees?.filter(x=>x?.firstName).slice(0, 5).map((attendee, index) => (
|
|
||||||
<View
|
|
||||||
key={attendee?.uid}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: index * 19,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
borderRadius: 50,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#f2f2f2',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: attendee.eventColor || colorMap.pink,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{attendee.pfp ? (
|
|
||||||
<CachedImage
|
|
||||||
source={{uri: attendee.pfp}}
|
|
||||||
style={{width: '100%', height: '100%'}}
|
|
||||||
cacheKey={attendee.pfp}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}>
|
|
||||||
<Text style={{
|
|
||||||
color: 'white',
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Manrope_600SemiBold",
|
|
||||||
}}>
|
|
||||||
{attendee?.firstName?.at(0)}
|
|
||||||
{attendee?.lastName?.at(0)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
{attendees.length > 3 && (
|
|
||||||
<View style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 3 * 19,
|
|
||||||
width: 27.32,
|
|
||||||
height: 27.32,
|
|
||||||
borderRadius: 50,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#f2f2f2',
|
|
||||||
backgroundColor: colorMap.pink,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}>
|
|
||||||
<Text style={{
|
|
||||||
color: 'white',
|
|
||||||
fontFamily: "Manrope_600SemiBold",
|
|
||||||
fontSize: 12,
|
|
||||||
}}>
|
|
||||||
+{attendees.length - 3}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|
||||||
({calendarHeight}) => {
|
|
||||||
const {data: events, isLoading} = useGetEvents();
|
|
||||||
const {profileData, user} = useAuthContext();
|
|
||||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
|
||||||
const [mode, setMode] = useAtom(modeAtom);
|
|
||||||
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
|
||||||
|
|
||||||
//tablet view filter
|
|
||||||
const [selectedUser] = useAtom(selectedUserAtom);
|
|
||||||
|
|
||||||
const setEditVisible = useSetAtom(editVisibleAtom);
|
|
||||||
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
|
|
||||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
|
||||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
|
||||||
|
|
||||||
const {isSyncing} = useSyncEvents()
|
|
||||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
|
||||||
useCalSync()
|
|
||||||
|
|
||||||
const todaysDate = new Date();
|
|
||||||
|
|
||||||
const handlePressEvent = useCallback(
|
|
||||||
(event: CalendarEvent) => {
|
|
||||||
if (mode === "day" || mode === "week" || mode === "3days") {
|
|
||||||
setEditVisible(true);
|
|
||||||
setEventForEdit(event);
|
|
||||||
} else {
|
|
||||||
setMode("day");
|
|
||||||
setSelectedDate(event.start);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setEditVisible, setEventForEdit, mode]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePressCell = useCallback(
|
|
||||||
(date: Date) => {
|
|
||||||
if (mode === "day" || mode === "week" || mode === "3days") {
|
|
||||||
setSelectedNewEndDate(date);
|
|
||||||
} else {
|
|
||||||
setMode("day");
|
|
||||||
setSelectedDate(date);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[mode, setSelectedNewEndDate, setSelectedDate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePressDayHeader = useCallback(
|
|
||||||
(date: Date) => {
|
|
||||||
if (mode === "day") {
|
|
||||||
setIsAllDay(true);
|
|
||||||
setSelectedNewEndDate(date);
|
|
||||||
setEditVisible(true);
|
|
||||||
}
|
|
||||||
if (mode === 'week' || mode === '3days') {
|
|
||||||
setSelectedDate(date)
|
|
||||||
setMode("day")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[mode, setSelectedNewEndDate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSwipeEnd = useCallback(
|
|
||||||
(date: Date) => {
|
|
||||||
setSelectedDate(date);
|
|
||||||
},
|
|
||||||
[setSelectedDate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const memoizedEventCellStyle = useCallback(
|
|
||||||
(event: CalendarEvent) => {
|
|
||||||
let eventColor = event.eventColor;
|
|
||||||
if (!isFamilyView && (event.attendees?.includes(user?.uid!) || event.creatorId! === user?.uid)) {
|
|
||||||
eventColor = profileData?.eventColor ?? colorMap.teal;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {backgroundColor: eventColor, fontSize: 14}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const memoizedWeekStartsOn = useMemo(
|
|
||||||
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
|
|
||||||
[profileData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
|
||||||
return (
|
|
||||||
date1.getDate() === date2.getDate() &&
|
|
||||||
date1.getMonth() === date2.getMonth() &&
|
|
||||||
date1.getFullYear() === date2.getFullYear()
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const dayHeaderColor = useMemo(() => {
|
|
||||||
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
|
|
||||||
}, [selectedDate, mode]);
|
|
||||||
|
|
||||||
const dateStyle = useMemo(() => {
|
|
||||||
if (mode === "week" || mode === "3days") return undefined;
|
|
||||||
return isSameDate(todaysDate, selectedDate) && mode === "day"
|
|
||||||
? styles.dayHeader
|
|
||||||
: styles.otherDayHeader;
|
|
||||||
}, [selectedDate, mode]);
|
|
||||||
|
|
||||||
const memoizedHeaderContentStyle = useMemo(() => {
|
|
||||||
if (mode === "day") {
|
|
||||||
return styles.dayModeHeader;
|
|
||||||
} else if (mode === "week" || mode === "3days") {
|
|
||||||
return styles.weekModeHeader;
|
|
||||||
} else if (mode === "month") {
|
|
||||||
return styles.monthModeHeader;
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}, [mode]);
|
|
||||||
const {enrichedEvents, filteredEvents} = useMemo(() => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const startOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
|
|
||||||
const endOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
|
|
||||||
|
|
||||||
let eventsToFilter = events;
|
|
||||||
|
|
||||||
if (selectedUser && Device.deviceType === DeviceType.TABLET) {
|
|
||||||
eventsToFilter = events?.filter(event =>
|
|
||||||
event.attendees?.includes(selectedUser.uid) ||
|
|
||||||
event.creatorId === selectedUser.uid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredEvents =
|
|
||||||
eventsToFilter?.filter(
|
|
||||||
(event) =>
|
|
||||||
event.start &&
|
|
||||||
event.end &&
|
|
||||||
isWithinInterval(event.start, {
|
|
||||||
start: subDays(selectedDate, startOffset),
|
|
||||||
end: addDays(selectedDate, endOffset),
|
|
||||||
}) &&
|
|
||||||
isWithinInterval(event.end, {
|
|
||||||
start: subDays(selectedDate, startOffset),
|
|
||||||
end: addDays(selectedDate, endOffset),
|
|
||||||
})
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
const enrichedEvents = filteredEvents.reduce((acc, event) => {
|
|
||||||
const dateKey = event.start.toISOString().split("T")[0];
|
|
||||||
acc[dateKey] = acc[dateKey] || [];
|
|
||||||
acc[dateKey].push({
|
|
||||||
...event,
|
|
||||||
overlapPosition: false,
|
|
||||||
overlapCount: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
acc[dateKey].sort((a: { start: any; }, b: { start: any; }) => compareAsc(a.start, b.start));
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, CalendarEvent[]>);
|
|
||||||
|
|
||||||
const endTime = Date.now();
|
|
||||||
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
|
||||||
|
|
||||||
return {enrichedEvents, filteredEvents};
|
|
||||||
}, [events, selectedDate, mode, selectedUser]);
|
|
||||||
|
|
||||||
const renderCustomDateForMonth = (date: Date) => {
|
|
||||||
const circleStyle = useMemo<ViewStyle>(
|
|
||||||
() => ({
|
|
||||||
width: 30,
|
|
||||||
height: 30,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
borderRadius: 15,
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultStyle = useMemo<ViewStyle>(
|
|
||||||
() => ({
|
|
||||||
...circleStyle,
|
|
||||||
}),
|
|
||||||
[circleStyle]
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentDateStyle = useMemo<ViewStyle>(
|
|
||||||
() => ({
|
|
||||||
...circleStyle,
|
|
||||||
backgroundColor: "#4184f2",
|
|
||||||
}),
|
|
||||||
[circleStyle]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderDate = useCallback(
|
|
||||||
(date: Date) => {
|
|
||||||
const isCurrentDate = isSameDate(todaysDate, date);
|
|
||||||
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{alignItems: "center"}}>
|
|
||||||
<View style={appliedStyle}>
|
|
||||||
<Text style={{color: isCurrentDate ? "white" : "black"}}>
|
|
||||||
{date.getDate()}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[todaysDate, currentDateStyle, defaultStyle]
|
|
||||||
);
|
|
||||||
|
|
||||||
return renderDate(date);
|
|
||||||
};
|
|
||||||
|
|
||||||
const processedEvents = useMemo(() => {
|
|
||||||
return processEventsForSideBySide(filteredEvents);
|
|
||||||
}, [filteredEvents]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOffsetMinutes(getTotalMinutes());
|
|
||||||
}, [events, mode]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
{isSyncing && <Text>Syncing...</Text>}
|
|
||||||
<ActivityIndicator size="large" color="#0000ff"/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isSyncing && (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
{isSyncing && <Text>Syncing...</Text>}
|
|
||||||
<ActivityIndicator size="large" color="#0000ff"/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<Calendar
|
|
||||||
bodyContainerStyle={styles.calHeader}
|
|
||||||
swipeEnabled
|
|
||||||
mode={mode}
|
|
||||||
sortedMonthView
|
|
||||||
events={filteredEvents}
|
|
||||||
// renderEvent={renderEvent}
|
|
||||||
eventCellStyle={memoizedEventCellStyle}
|
|
||||||
allDayEventCellStyle={memoizedEventCellStyle}
|
|
||||||
// enableEnrichedEvents={true}
|
|
||||||
// enrichedEventsByDate={enrichedEvents}
|
|
||||||
onPressEvent={handlePressEvent}
|
|
||||||
weekStartsOn={memoizedWeekStartsOn}
|
|
||||||
height={calendarHeight}
|
|
||||||
activeDate={todaysDate}
|
|
||||||
date={selectedDate}
|
|
||||||
onPressCell={handlePressCell}
|
|
||||||
headerContentStyle={memoizedHeaderContentStyle}
|
|
||||||
onSwipeEnd={handleSwipeEnd}
|
|
||||||
scrollOffsetMinutes={offsetMinutes}
|
|
||||||
theme={{
|
|
||||||
palette: {
|
|
||||||
nowIndicator: profileData?.eventColor || "#fd1575",
|
|
||||||
gray: {
|
|
||||||
"100": "#e8eaed",
|
|
||||||
"200": "#e8eaed",
|
|
||||||
"500": "#b7b7b7",
|
|
||||||
"800": "#919191",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
typography: {
|
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
|
||||||
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
|
|
||||||
xl: {
|
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
moreLabel: {},
|
|
||||||
xs: {fontSize: 10},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
dayHeaderStyle={dateStyle}
|
|
||||||
dayHeaderHighlightColor={"white"}
|
|
||||||
showAdjacentMonths
|
|
||||||
headerContainerStyle={mode !== "month" ? {
|
|
||||||
overflow: "hidden",
|
|
||||||
} : {}}
|
|
||||||
hourStyle={styles.hourStyle}
|
|
||||||
onPressDateHeader={handlePressDayHeader}
|
|
||||||
ampm
|
|
||||||
// renderCustomDateForMonth={renderCustomDateForMonth}
|
|
||||||
/>
|
|
||||||
<View style={{backgroundColor: 'white', height: 50, width: '100%'}}/>
|
|
||||||
</>
|
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
return mode === "month"
|
||||||
|
? <MonthCalendar {...props} />
|
||||||
|
: <DetailedCalendar {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
segmentslblStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Manrope_600SemiBold",
|
|
||||||
},
|
|
||||||
calHeader: {
|
|
||||||
borderWidth: 0,
|
|
||||||
paddingBottom: 60,
|
|
||||||
},
|
|
||||||
dayModeHeader: {
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignContent: "center",
|
|
||||||
width: 38,
|
|
||||||
right: 42,
|
|
||||||
height: 13,
|
|
||||||
},
|
|
||||||
weekModeHeader: {},
|
|
||||||
monthModeHeader: {},
|
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
zIndex: 100,
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
|
||||||
},
|
|
||||||
dayHeader: {
|
|
||||||
backgroundColor: "#4184f2",
|
|
||||||
aspectRatio: 1,
|
|
||||||
borderRadius: 100,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
otherDayHeader: {
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
color: "#919191",
|
|
||||||
aspectRatio: 1,
|
|
||||||
borderRadius: 100,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
hourStyle: {
|
|
||||||
color: "#5f6368",
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Manrope_500Medium",
|
|
||||||
},
|
|
||||||
eventCell: {
|
|
||||||
flex: 1,
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: 4,
|
|
||||||
height: '100%',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
alignItems: 'center',
|
||||||
eventTitle: {
|
position: 'absolute',
|
||||||
color: 'white',
|
width: '100%',
|
||||||
fontSize: 12,
|
height: '100%',
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
zIndex: 100,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default EventCalendar;
|
117
components/pages/calendar/EventCell.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||||
|
import {Text} from 'react-native-ui-lib';
|
||||||
|
import CachedImage from 'expo-cached-image';
|
||||||
|
import {format} from 'date-fns';
|
||||||
|
import {colorMap} from '@/constants/colorMap';
|
||||||
|
|
||||||
|
interface EventCellProps {
|
||||||
|
event: any;
|
||||||
|
onPress: (event: any) => void;
|
||||||
|
attendees: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventCell: React.FC<EventCellProps> = React.memo((
|
||||||
|
{
|
||||||
|
event,
|
||||||
|
onPress,
|
||||||
|
attendees
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => onPress(event)}
|
||||||
|
style={[styles.eventCell, {backgroundColor: event.eventColor}]}
|
||||||
|
>
|
||||||
|
<Text style={styles.eventTitle} numberOfLines={1}>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.eventTitle, {fontSize: 10, opacity: 0.8}]}>
|
||||||
|
{format(new Date(event.start.dateTime), 'h:mm a')} - {format(new Date(event.end.dateTime), 'h:mm a')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{attendees.length > 0 && (
|
||||||
|
<View style={styles.attendeesContainer}>
|
||||||
|
{attendees.slice(0, 3).map((attendee, index) => (
|
||||||
|
<View
|
||||||
|
key={attendee?.uid}
|
||||||
|
style={[styles.attendeeIcon, {left: index * 19}]}
|
||||||
|
>
|
||||||
|
{attendee.pfp ? (
|
||||||
|
<CachedImage
|
||||||
|
source={{uri: attendee.pfp}}
|
||||||
|
style={styles.attendeeImage}
|
||||||
|
cacheKey={attendee.pfp}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.attendeeInitials}>
|
||||||
|
{attendee?.firstName?.at(0)}
|
||||||
|
{attendee?.lastName?.at(0)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{attendees.length > 3 && (
|
||||||
|
<View style={[styles.attendeeCount, {left: 3 * 19}]}>
|
||||||
|
<Text style={styles.attendeeCountText}>
|
||||||
|
+{attendees.length - 3}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
eventCell: {
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 8,
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
eventTitle: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
},
|
||||||
|
attendeesContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginTop: 8,
|
||||||
|
height: 27.32,
|
||||||
|
},
|
||||||
|
attendeeIcon: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 50,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#f2f2f2',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
attendeeImage: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
attendeeInitials: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
},
|
||||||
|
attendeeCount: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 27.32,
|
||||||
|
height: 27.32,
|
||||||
|
borderRadius: 50,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#f2f2f2',
|
||||||
|
backgroundColor: colorMap.pink,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
attendeeCountText: {
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
});
|
723
components/pages/calendar/MonthCalendar.tsx
Normal file
@ -0,0 +1,723 @@
|
|||||||
|
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||||
|
import {Calendar} from "react-native-big-calendar";
|
||||||
|
import {ActivityIndicator, StyleSheet, TouchableOpacity, View, ViewStyle} from "react-native";
|
||||||
|
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||||
|
import {useAtom, useSetAtom} from "jotai";
|
||||||
|
import {
|
||||||
|
editVisibleAtom,
|
||||||
|
eventForEditAtom,
|
||||||
|
isAllDayAtom,
|
||||||
|
isFamilyViewAtom,
|
||||||
|
modeAtom,
|
||||||
|
selectedDateAtom,
|
||||||
|
selectedNewEventDateAtom,
|
||||||
|
selectedUserAtom,
|
||||||
|
} from "@/components/pages/calendar/atoms";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
||||||
|
import {Text} from "react-native-ui-lib";
|
||||||
|
import {addDays, compareAsc, format, isWithinInterval, subDays} from "date-fns";
|
||||||
|
import {useCalSync} from "@/hooks/useCalSync";
|
||||||
|
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
|
||||||
|
import {colorMap} from "@/constants/colorMap";
|
||||||
|
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
|
import CachedImage from "expo-cached-image";
|
||||||
|
import { DeviceType } from "expo-device";
|
||||||
|
import * as Device from "expo-device"
|
||||||
|
|
||||||
|
interface EventCalendarProps {
|
||||||
|
calendarHeight: number;
|
||||||
|
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
|
||||||
|
calendarWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTotalMinutes = () => {
|
||||||
|
const date = new Date();
|
||||||
|
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const processEventsForSideBySide = (events: CalendarEvent[]) => {
|
||||||
|
if (!events) return [];
|
||||||
|
|
||||||
|
const timeSlots: { [key: string]: CalendarEvent[] } = {};
|
||||||
|
|
||||||
|
events.forEach(event => {
|
||||||
|
const startDate = new Date(event.start);
|
||||||
|
const endDate = new Date(event.end);
|
||||||
|
|
||||||
|
// If it's an all-day event, mark it and add it directly
|
||||||
|
if (event.allDay) {
|
||||||
|
const key = `${startDate.toISOString().split('T')[0]}-allday`;
|
||||||
|
if (!timeSlots[key]) timeSlots[key] = [];
|
||||||
|
timeSlots[key].push({
|
||||||
|
...event,
|
||||||
|
isAllDayEvent: true,
|
||||||
|
allDay: true,
|
||||||
|
width: 1,
|
||||||
|
xPos: 0
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multi-day events
|
||||||
|
if (startDate.toDateString() !== endDate.toDateString()) {
|
||||||
|
// Create array of dates between start and end
|
||||||
|
const dates = [];
|
||||||
|
let currentDate = new Date(startDate);
|
||||||
|
|
||||||
|
while (currentDate <= endDate) {
|
||||||
|
dates.push(new Date(currentDate));
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create segments for each day
|
||||||
|
dates.forEach((date, index) => {
|
||||||
|
const isFirstDay = index === 0;
|
||||||
|
const isLastDay = index === dates.length - 1;
|
||||||
|
|
||||||
|
let segmentStart, segmentEnd;
|
||||||
|
|
||||||
|
if (isFirstDay) {
|
||||||
|
// First day: use original start time to end of day
|
||||||
|
segmentStart = new Date(startDate);
|
||||||
|
segmentEnd = new Date(date);
|
||||||
|
segmentEnd.setHours(23, 59, 59);
|
||||||
|
} else if (isLastDay) {
|
||||||
|
// Last day: use start of day to original end time
|
||||||
|
segmentStart = new Date(date);
|
||||||
|
segmentStart.setHours(0, 0, 0);
|
||||||
|
segmentEnd = new Date(endDate);
|
||||||
|
} else {
|
||||||
|
// Middle days: full day
|
||||||
|
segmentStart = new Date(date);
|
||||||
|
segmentStart.setHours(0, 0, 0);
|
||||||
|
segmentEnd = new Date(date);
|
||||||
|
segmentEnd.setHours(23, 59, 59);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${segmentStart.toISOString().split('T')[0]}-${segmentStart.getHours()}`;
|
||||||
|
if (!timeSlots[key]) timeSlots[key] = [];
|
||||||
|
|
||||||
|
timeSlots[key].push({
|
||||||
|
...event,
|
||||||
|
start: segmentStart,
|
||||||
|
end: segmentEnd,
|
||||||
|
isMultiDaySegment: true,
|
||||||
|
isFirstDay,
|
||||||
|
isLastDay,
|
||||||
|
originalStart: startDate,
|
||||||
|
originalEnd: endDate,
|
||||||
|
allDay: true // Mark multi-day events as all-day events
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Regular event
|
||||||
|
const key = `${startDate.toISOString().split('T')[0]}-${startDate.getHours()}`;
|
||||||
|
if (!timeSlots[key]) timeSlots[key] = [];
|
||||||
|
timeSlots[key].push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process all time slots
|
||||||
|
return Object.values(timeSlots).flatMap(slotEvents => {
|
||||||
|
// Sort events by start time
|
||||||
|
slotEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||||
|
|
||||||
|
// Find overlapping events (only for non-all-day events)
|
||||||
|
return slotEvents.map((event, index) => {
|
||||||
|
// If it's an all-day or multi-day event, return as is
|
||||||
|
if (event.allDay || event.isMultiDaySegment) {
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
width: 1,
|
||||||
|
xPos: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular events
|
||||||
|
const overlappingEvents = slotEvents.filter((otherEvent, otherIndex) => {
|
||||||
|
if (index === otherIndex || otherEvent.allDay || otherEvent.isMultiDaySegment) return false;
|
||||||
|
const eventStart = new Date(event.start);
|
||||||
|
const eventEnd = new Date(event.end);
|
||||||
|
const otherStart = new Date(otherEvent.start);
|
||||||
|
const otherEnd = new Date(otherEvent.end);
|
||||||
|
return (eventStart < otherEnd && eventEnd > otherStart);
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = overlappingEvents.length + 1;
|
||||||
|
const position = index % total;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
width: 1 / total,
|
||||||
|
xPos: position / total
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEvent = (event: CalendarEvent & {
|
||||||
|
width: number;
|
||||||
|
xPos: number;
|
||||||
|
isMultiDaySegment?: boolean;
|
||||||
|
isFirstDay?: boolean;
|
||||||
|
isLastDay?: boolean;
|
||||||
|
originalStart?: Date;
|
||||||
|
originalEnd?: Date;
|
||||||
|
isAllDayEvent?: boolean;
|
||||||
|
allDay?: boolean;
|
||||||
|
eventColor?: string;
|
||||||
|
attendees?: string[];
|
||||||
|
creatorId?: string;
|
||||||
|
pfp?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
notes?: string;
|
||||||
|
hideHours?: boolean;
|
||||||
|
}, props: any) => {
|
||||||
|
const {data: familyMembers} = useGetFamilyMembers();
|
||||||
|
|
||||||
|
const attendees = useMemo(() => {
|
||||||
|
if (!familyMembers || !event.attendees) return event?.creatorId ? [event?.creatorId] : [];
|
||||||
|
return familyMembers.filter(member => event?.attendees?.includes(member?.uid!));
|
||||||
|
}, [familyMembers, event.attendees]);
|
||||||
|
|
||||||
|
if (event.allDay && !!event.isMultiDaySegment) {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
{...props}
|
||||||
|
style={[
|
||||||
|
props.style,
|
||||||
|
{
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.allDayEventText} numberOfLines={1}>
|
||||||
|
{event.title}
|
||||||
|
{event.isMultiDaySegment &&
|
||||||
|
` (${format(new Date(event.start), 'MMM d')} - ${format(new Date(event.end), 'MMM d')})`
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalStyle = Array.isArray(props.style) ? props.style[0] : props.style;
|
||||||
|
|
||||||
|
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
|
||||||
|
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
|
||||||
|
|
||||||
|
const hourHeight = props.hourHeight || 60;
|
||||||
|
const startHour = startDate.getHours();
|
||||||
|
const startMinutes = startDate.getMinutes();
|
||||||
|
const topPosition = (startHour + startMinutes / 60) * hourHeight;
|
||||||
|
|
||||||
|
const endHour = endDate.getHours();
|
||||||
|
const endMinutes = endDate.getMinutes();
|
||||||
|
const duration = (endHour + endMinutes / 60) - (startHour + startMinutes / 60);
|
||||||
|
const height = duration * hourHeight;
|
||||||
|
|
||||||
|
const formatTime = (date: Date) => {
|
||||||
|
const hours = date.getHours();
|
||||||
|
const minutes = date.getMinutes();
|
||||||
|
const ampm = hours >= 12 ? 'pm' : 'am';
|
||||||
|
const formattedHours = hours % 12 || 12;
|
||||||
|
const formattedMinutes = minutes.toString().padStart(2, '0');
|
||||||
|
return `${formattedHours}:${formattedMinutes}${ampm}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeString = event.isMultiDaySegment
|
||||||
|
? event.isFirstDay
|
||||||
|
? `${formatTime(startDate)} → 12:00PM`
|
||||||
|
: event.isLastDay
|
||||||
|
? `12:00am → ${formatTime(endDate)}`
|
||||||
|
: 'All day'
|
||||||
|
: `${formatTime(startDate)} - ${formatTime(endDate)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
{...props}
|
||||||
|
style={[
|
||||||
|
originalStyle,
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
width: `${event.width * 100}%`,
|
||||||
|
left: `${event.xPos * 100}%`,
|
||||||
|
top: topPosition,
|
||||||
|
height: height,
|
||||||
|
zIndex: event.isMultiDaySegment ? 1 : 2,
|
||||||
|
shadowRadius: 2,
|
||||||
|
overflow: "hidden"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: event.eventColor,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 8,
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 4
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
opacity: 0.8
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timeString}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Attendees Section */}
|
||||||
|
{attendees?.length > 0 && (
|
||||||
|
<View style={{flexDirection: 'row', marginTop: 8, height: 27.32}}>
|
||||||
|
{attendees?.filter(x=>x?.firstName).slice(0, 5).map((attendee, index) => (
|
||||||
|
<View
|
||||||
|
key={attendee?.uid}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: index * 19,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 50,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#f2f2f2',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: attendee.eventColor || colorMap.pink,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{attendee.pfp ? (
|
||||||
|
<CachedImage
|
||||||
|
source={{uri: attendee.pfp}}
|
||||||
|
style={{width: '100%', height: '100%'}}
|
||||||
|
cacheKey={attendee.pfp}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<Text style={{
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
}}>
|
||||||
|
{attendee?.firstName?.at(0)}
|
||||||
|
{attendee?.lastName?.at(0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{attendees.length > 3 && (
|
||||||
|
<View style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 3 * 19,
|
||||||
|
width: 27.32,
|
||||||
|
height: 27.32,
|
||||||
|
borderRadius: 50,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#f2f2f2',
|
||||||
|
backgroundColor: colorMap.pink,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}>
|
||||||
|
<Text style={{
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 12,
|
||||||
|
}}>
|
||||||
|
+{attendees.length - 3}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const MonthCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||||
|
({calendarHeight}) => {
|
||||||
|
const {data: events, isLoading} = useGetEvents();
|
||||||
|
const {profileData, user} = useAuthContext();
|
||||||
|
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||||
|
const [mode, setMode] = useAtom(modeAtom);
|
||||||
|
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
||||||
|
|
||||||
|
//tablet view filter
|
||||||
|
const [selectedUser] = useAtom(selectedUserAtom);
|
||||||
|
|
||||||
|
const setEditVisible = useSetAtom(editVisibleAtom);
|
||||||
|
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
|
||||||
|
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||||
|
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||||
|
|
||||||
|
const {isSyncing} = useSyncEvents()
|
||||||
|
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||||
|
useCalSync()
|
||||||
|
|
||||||
|
const todaysDate = new Date();
|
||||||
|
|
||||||
|
const handlePressEvent = useCallback(
|
||||||
|
(event: CalendarEvent) => {
|
||||||
|
if (mode === "day" || mode === "week" || mode === "3days") {
|
||||||
|
setEditVisible(true);
|
||||||
|
setEventForEdit(event);
|
||||||
|
} else {
|
||||||
|
setMode("day");
|
||||||
|
setSelectedDate(event.start);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setEditVisible, setEventForEdit, mode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePressCell = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
if (mode === "day" || mode === "week" || mode === "3days") {
|
||||||
|
setSelectedNewEndDate(date);
|
||||||
|
} else {
|
||||||
|
setMode("day");
|
||||||
|
setSelectedDate(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mode, setSelectedNewEndDate, setSelectedDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePressDayHeader = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
if (mode === "day") {
|
||||||
|
setIsAllDay(true);
|
||||||
|
setSelectedNewEndDate(date);
|
||||||
|
setEditVisible(true);
|
||||||
|
}
|
||||||
|
if (mode === 'week' || mode === '3days') {
|
||||||
|
setSelectedDate(date)
|
||||||
|
setMode("day")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mode, setSelectedNewEndDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSwipeEnd = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
setSelectedDate(date);
|
||||||
|
},
|
||||||
|
[setSelectedDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedEventCellStyle = useCallback(
|
||||||
|
(event: CalendarEvent) => {
|
||||||
|
let eventColor = event.eventColor;
|
||||||
|
if (!isFamilyView && (event.attendees?.includes(user?.uid!) || event.creatorId! === user?.uid)) {
|
||||||
|
eventColor = profileData?.eventColor ?? colorMap.teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {backgroundColor: eventColor, fontSize: 14}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoizedWeekStartsOn = useMemo(
|
||||||
|
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
|
||||||
|
[profileData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||||
|
return (
|
||||||
|
date1.getDate() === date2.getDate() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getFullYear() === date2.getFullYear()
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dayHeaderColor = useMemo(() => {
|
||||||
|
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
|
||||||
|
}, [selectedDate, mode]);
|
||||||
|
|
||||||
|
const dateStyle = useMemo(() => {
|
||||||
|
if (mode === "week" || mode === "3days") return undefined;
|
||||||
|
return isSameDate(todaysDate, selectedDate) && mode === "day"
|
||||||
|
? styles.dayHeader
|
||||||
|
: styles.otherDayHeader;
|
||||||
|
}, [selectedDate, mode]);
|
||||||
|
|
||||||
|
const memoizedHeaderContentStyle = useMemo(() => {
|
||||||
|
if (mode === "day") {
|
||||||
|
return styles.dayModeHeader;
|
||||||
|
} else if (mode === "week" || mode === "3days") {
|
||||||
|
return styles.weekModeHeader;
|
||||||
|
} else if (mode === "month") {
|
||||||
|
return styles.monthModeHeader;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
const {enrichedEvents, filteredEvents} = useMemo(() => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const startOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
|
||||||
|
const endOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
|
||||||
|
|
||||||
|
let eventsToFilter = events;
|
||||||
|
|
||||||
|
if (selectedUser && Device.deviceType === DeviceType.TABLET) {
|
||||||
|
eventsToFilter = events?.filter(event =>
|
||||||
|
event.attendees?.includes(selectedUser.uid) ||
|
||||||
|
event.creatorId === selectedUser.uid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredEvents =
|
||||||
|
eventsToFilter?.filter(
|
||||||
|
(event) =>
|
||||||
|
event.start &&
|
||||||
|
event.end &&
|
||||||
|
isWithinInterval(event.start, {
|
||||||
|
start: subDays(selectedDate, startOffset),
|
||||||
|
end: addDays(selectedDate, endOffset),
|
||||||
|
}) &&
|
||||||
|
isWithinInterval(event.end, {
|
||||||
|
start: subDays(selectedDate, startOffset),
|
||||||
|
end: addDays(selectedDate, endOffset),
|
||||||
|
})
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const enrichedEvents = filteredEvents.reduce((acc, event) => {
|
||||||
|
const dateKey = event.start.toISOString().split("T")[0];
|
||||||
|
acc[dateKey] = acc[dateKey] || [];
|
||||||
|
acc[dateKey].push({
|
||||||
|
...event,
|
||||||
|
overlapPosition: false,
|
||||||
|
overlapCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
acc[dateKey].sort((a: { start: any; }, b: { start: any; }) => compareAsc(a.start, b.start));
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, CalendarEvent[]>);
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||||
|
|
||||||
|
return {enrichedEvents, filteredEvents};
|
||||||
|
}, [events, selectedDate, mode, selectedUser]);
|
||||||
|
|
||||||
|
const renderCustomDateForMonth = (date: Date) => {
|
||||||
|
const circleStyle = useMemo<ViewStyle>(
|
||||||
|
() => ({
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: 15,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultStyle = useMemo<ViewStyle>(
|
||||||
|
() => ({
|
||||||
|
...circleStyle,
|
||||||
|
}),
|
||||||
|
[circleStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentDateStyle = useMemo<ViewStyle>(
|
||||||
|
() => ({
|
||||||
|
...circleStyle,
|
||||||
|
backgroundColor: "#4184f2",
|
||||||
|
}),
|
||||||
|
[circleStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDate = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
const isCurrentDate = isSameDate(todaysDate, date);
|
||||||
|
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{alignItems: "center"}}>
|
||||||
|
<View style={appliedStyle}>
|
||||||
|
<Text style={{color: isCurrentDate ? "white" : "black"}}>
|
||||||
|
{date.getDate()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[todaysDate, currentDateStyle, defaultStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderDate(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processedEvents = useMemo(() => {
|
||||||
|
return processEventsForSideBySide(filteredEvents);
|
||||||
|
}, [filteredEvents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOffsetMinutes(getTotalMinutes());
|
||||||
|
}, [events, mode]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
{isSyncing && <Text>Syncing...</Text>}
|
||||||
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isSyncing && (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
{isSyncing && <Text>Syncing...</Text>}
|
||||||
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Calendar
|
||||||
|
bodyContainerStyle={styles.calHeader}
|
||||||
|
swipeEnabled
|
||||||
|
mode={mode}
|
||||||
|
sortedMonthView
|
||||||
|
events={filteredEvents}
|
||||||
|
// renderEvent={renderEvent}
|
||||||
|
eventCellStyle={memoizedEventCellStyle}
|
||||||
|
allDayEventCellStyle={memoizedEventCellStyle}
|
||||||
|
// enableEnrichedEvents={true}
|
||||||
|
// enrichedEventsByDate={enrichedEvents}
|
||||||
|
onPressEvent={handlePressEvent}
|
||||||
|
weekStartsOn={memoizedWeekStartsOn}
|
||||||
|
height={calendarHeight}
|
||||||
|
activeDate={todaysDate}
|
||||||
|
date={selectedDate}
|
||||||
|
onPressCell={handlePressCell}
|
||||||
|
headerContentStyle={memoizedHeaderContentStyle}
|
||||||
|
onSwipeEnd={handleSwipeEnd}
|
||||||
|
scrollOffsetMinutes={offsetMinutes}
|
||||||
|
theme={{
|
||||||
|
palette: {
|
||||||
|
nowIndicator: profileData?.eventColor || "#fd1575",
|
||||||
|
gray: {
|
||||||
|
"100": "#e8eaed",
|
||||||
|
"200": "#e8eaed",
|
||||||
|
"500": "#b7b7b7",
|
||||||
|
"800": "#919191",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
|
||||||
|
xl: {
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
moreLabel: {},
|
||||||
|
xs: {fontSize: 10},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
dayHeaderStyle={dateStyle}
|
||||||
|
dayHeaderHighlightColor={"white"}
|
||||||
|
showAdjacentMonths
|
||||||
|
headerContainerStyle={mode !== "month" ? {
|
||||||
|
overflow: "hidden",
|
||||||
|
} : {}}
|
||||||
|
hourStyle={styles.hourStyle}
|
||||||
|
onPressDateHeader={handlePressDayHeader}
|
||||||
|
ampm
|
||||||
|
// renderCustomDateForMonth={renderCustomDateForMonth}
|
||||||
|
/>
|
||||||
|
<View style={{backgroundColor: 'white', height: 50, width: '100%'}}/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
segmentslblStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
},
|
||||||
|
calHeader: {
|
||||||
|
borderWidth: 0,
|
||||||
|
paddingBottom: 60,
|
||||||
|
},
|
||||||
|
dayModeHeader: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignContent: "center",
|
||||||
|
width: 38,
|
||||||
|
right: 42,
|
||||||
|
height: 13,
|
||||||
|
},
|
||||||
|
weekModeHeader: {},
|
||||||
|
monthModeHeader: {},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
zIndex: 100,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||||
|
},
|
||||||
|
dayHeader: {
|
||||||
|
backgroundColor: "#4184f2",
|
||||||
|
aspectRatio: 1,
|
||||||
|
borderRadius: 100,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
otherDayHeader: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "#919191",
|
||||||
|
aspectRatio: 1,
|
||||||
|
borderRadius: 100,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
hourStyle: {
|
||||||
|
color: "#5f6368",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
},
|
||||||
|
eventCell: {
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 4,
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
eventTitle: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
},
|
||||||
|
});
|
@ -1 +0,0 @@
|
|||||||
|
|
45
components/pages/calendar/useCalendarControls.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import debounce from 'debounce';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import {
|
||||||
|
editVisibleAtom,
|
||||||
|
eventForEditAtom,
|
||||||
|
selectedNewEventDateAtom,
|
||||||
|
selectedDateAtom,
|
||||||
|
} from '@/components/pages/calendar/atoms';
|
||||||
|
import {DateOrDateTime} from "@howljs/calendar-kit";
|
||||||
|
|
||||||
|
export const useCalendarControls = (events: any[]) => {
|
||||||
|
const setEditVisible = useSetAtom(editVisibleAtom);
|
||||||
|
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||||
|
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||||
|
const setSelectedDate = useSetAtom(selectedDateAtom);
|
||||||
|
|
||||||
|
const handlePressEvent = useCallback((event: any) => {
|
||||||
|
const foundEvent = events?.find(x => x.id === event.id);
|
||||||
|
setEditVisible(true);
|
||||||
|
setEventForEdit(foundEvent!);
|
||||||
|
}, [events, setEditVisible, setEventForEdit]);
|
||||||
|
|
||||||
|
const handlePressCell = useCallback((date: DateOrDateTime) => {
|
||||||
|
const selectedDate = new Date(date.dateTime!);
|
||||||
|
const minutes = selectedDate.getMinutes();
|
||||||
|
|
||||||
|
selectedDate.setMinutes(minutes - (minutes % 30), 0, 0); // Also sets seconds and milliseconds to 0
|
||||||
|
|
||||||
|
setSelectedNewEndDate(selectedDate);
|
||||||
|
}, [setSelectedNewEndDate]);
|
||||||
|
|
||||||
|
const debouncedOnDateChanged = useCallback(
|
||||||
|
debounce((date: string) => {
|
||||||
|
setSelectedDate(new Date(date));
|
||||||
|
}, 50),
|
||||||
|
[setSelectedDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handlePressEvent,
|
||||||
|
handlePressCell,
|
||||||
|
debouncedOnDateChanged
|
||||||
|
};
|
||||||
|
};
|
154
components/pages/calendar/useFormattedEvents.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { DeviceType } from 'expo-device';
|
||||||
|
import * as Device from 'expo-device';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
interface EventTimestamp {
|
||||||
|
seconds: number;
|
||||||
|
nanoseconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
start: Date | EventTimestamp;
|
||||||
|
startDate?: EventTimestamp;
|
||||||
|
end: Date | EventTimestamp;
|
||||||
|
endDate?: EventTimestamp;
|
||||||
|
allDay: boolean;
|
||||||
|
eventColor: string;
|
||||||
|
attendees?: string[];
|
||||||
|
creatorId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormattedEvent {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
start: { date: string } | { dateTime: string; timeZone: string };
|
||||||
|
end: { date: string } | { dateTime: string; timeZone: string };
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompute time constants
|
||||||
|
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const PERIOD_IN_MS = 45 * DAY_IN_MS;
|
||||||
|
const TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
|
// Memoize date range calculations
|
||||||
|
const getDateRangeKey = (timestamp: number) => Math.floor(timestamp / PERIOD_IN_MS);
|
||||||
|
|
||||||
|
// Optimized date validation and conversion
|
||||||
|
const getValidDate = (date: any, timestamp?: EventTimestamp): Date | null => {
|
||||||
|
try {
|
||||||
|
if (timestamp?.seconds) {
|
||||||
|
return new Date(timestamp.seconds * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date instanceof Date) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof date === 'string') {
|
||||||
|
return new Date(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Batch process events
|
||||||
|
const processEvents = async (
|
||||||
|
events: Event[],
|
||||||
|
selectedDate: Date,
|
||||||
|
selectedUser: { uid: string } | null
|
||||||
|
): Promise<FormattedEvent[]> => {
|
||||||
|
// Early return if no events
|
||||||
|
if (!events.length) return [];
|
||||||
|
|
||||||
|
// Pre-calculate constants
|
||||||
|
const currentRangeKey = getDateRangeKey(selectedDate.getTime());
|
||||||
|
const isTablet = Device.deviceType === DeviceType.TABLET;
|
||||||
|
const userId = selectedUser?.uid;
|
||||||
|
|
||||||
|
// Process in chunks to avoid blocking the main thread
|
||||||
|
const CHUNK_SIZE = 100;
|
||||||
|
const results: FormattedEvent[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < events.length; i += CHUNK_SIZE) {
|
||||||
|
const chunk = events.slice(i, i + CHUNK_SIZE);
|
||||||
|
|
||||||
|
// Process chunk and await to give UI thread a chance to breathe
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
for (const event of chunk) {
|
||||||
|
try {
|
||||||
|
// Quick user filter
|
||||||
|
if (isTablet && userId &&
|
||||||
|
!event.attendees?.includes(userId) &&
|
||||||
|
event.creatorId !== userId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate dates first
|
||||||
|
const startDate = getValidDate(event.start, event.startDate);
|
||||||
|
if (!startDate) continue;
|
||||||
|
|
||||||
|
const rangeKey = getDateRangeKey(startDate.getTime());
|
||||||
|
// Skip events outside our range
|
||||||
|
if (Math.abs(rangeKey - currentRangeKey) > 1) continue;
|
||||||
|
|
||||||
|
const endDate = getValidDate(event.end, event.endDate);
|
||||||
|
if (!endDate) continue;
|
||||||
|
|
||||||
|
if (event.allDay) {
|
||||||
|
const dateStr = format(startDate, 'yyyy-MM-dd');
|
||||||
|
const endDateStr = format(endDate, 'yyyy-MM-dd');
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
start: { date: dateStr },
|
||||||
|
end: { date: endDateStr },
|
||||||
|
color: event.eventColor
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
results.push({
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
start: {
|
||||||
|
dateTime: startDate.toISOString(),
|
||||||
|
timeZone: TIME_ZONE
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: endDate.toISOString(),
|
||||||
|
timeZone: TIME_ZONE
|
||||||
|
},
|
||||||
|
color: event.eventColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing event:', event.id, error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFormattedEvents = (
|
||||||
|
events: Event[],
|
||||||
|
selectedDate: Date,
|
||||||
|
selectedUser: { uid: string } | null
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['formattedEvents', events, selectedDate, selectedUser?.uid],
|
||||||
|
queryFn: () => processEvents(events, selectedDate, selectedUser),
|
||||||
|
enabled: events.length > 0,
|
||||||
|
staleTime: Infinity,
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
gcTime: Infinity
|
||||||
|
});
|
||||||
|
};
|
@ -25,6 +25,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import * as Device from "expo-device";
|
import * as Device from "expo-device";
|
||||||
import { DeviceType } from "expo-device";
|
import { DeviceType } from "expo-device";
|
||||||
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
|
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ const SignUpPage = () => {
|
|||||||
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
||||||
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
||||||
const { mutateAsync: signUp, isLoading } = useSignUp();
|
const { mutateAsync: signUp, isLoading } = useSignUp();
|
||||||
|
const {profileData} = useAuthContext();
|
||||||
|
|
||||||
const lnameRef = useRef<TextFieldRef>(null);
|
const lnameRef = useRef<TextFieldRef>(null);
|
||||||
const emailRef = useRef<TextFieldRef>(null);
|
const emailRef = useRef<TextFieldRef>(null);
|
||||||
@ -68,7 +70,12 @@ const SignUpPage = () => {
|
|||||||
|
|
||||||
const handleSignUp = async () => {
|
const handleSignUp = async () => {
|
||||||
await signUp({ email, password, firstName, lastName });
|
await signUp({ email, password, firstName, lastName });
|
||||||
router.replace("/(unauth)/cal_sync");
|
|
||||||
|
if (profileData?.userType === ProfileType.FAMILY_DEVICE) {
|
||||||
|
router.replace("/(auth)/calendar");
|
||||||
|
} else {
|
||||||
|
router.replace("/(unauth)/cal_sync");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -291,6 +291,13 @@ const MyProfile = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightPink)} disabled={takenColors.includes(colorMap.lightPink)}>
|
||||||
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightPink) ? 0.1 : 1}]} backgroundColor={colorMap.lightPink}>
|
||||||
|
{selectedColor == colorMap.lightPink && (
|
||||||
|
<AntDesign name="check" size={30} color="black" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.orange)} disabled={takenColors.includes(colorMap.orange)}>
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.orange)} disabled={takenColors.includes(colorMap.orange)}>
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.orange) ? 0.1 : 1}]} backgroundColor={colorMap.orange}>
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.orange) ? 0.1 : 1}]} backgroundColor={colorMap.orange}>
|
||||||
{selectedColor == colorMap.orange && (
|
{selectedColor == colorMap.orange && (
|
||||||
@ -298,13 +305,29 @@ const MyProfile = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)} disabled={takenColors.includes(colorMap.green)}>
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightOrange)} disabled={takenColors.includes(colorMap.lightOrange)}>
|
||||||
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightOrange) ? 0.1 : 1}]} backgroundColor={colorMap.lightOrange}>
|
||||||
|
{selectedColor == colorMap.lightOrange && (
|
||||||
|
<AntDesign name="check" size={30} color="black" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)}disabled={takenColors.includes(colorMap.green)}>
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.green) ? 0.1 : 1}]} backgroundColor={colorMap.green}>
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.green) ? 0.1 : 1}]} backgroundColor={colorMap.green}>
|
||||||
{selectedColor == colorMap.green && (
|
{selectedColor == colorMap.green && (
|
||||||
<AntDesign name="check" size={30} color="white" />
|
<AntDesign name="check" size={30} color="white" />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View row spread marginT-10>
|
||||||
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightGreen)} disabled={takenColors.includes(colorMap.lightGreen)}>
|
||||||
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightGreen) ? 0.1 : 1}]} backgroundColor={colorMap.lightGreen}>
|
||||||
|
{selectedColor == colorMap.lightGreen && (
|
||||||
|
<AntDesign name="check" size={30} color="black" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)} disabled={takenColors.includes(colorMap.teal)}>
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)} disabled={takenColors.includes(colorMap.teal)}>
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.teal) ? 0.1 : 1}]} backgroundColor={colorMap.teal}>
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.teal) ? 0.1 : 1}]} backgroundColor={colorMap.teal}>
|
||||||
{selectedColor == colorMap.teal && (
|
{selectedColor == colorMap.teal && (
|
||||||
@ -312,47 +335,24 @@ const MyProfile = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.purple)}disabled={takenColors.includes(colorMap.purple)}>
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightTeal)} disabled={takenColors.includes(colorMap.lightTeal)}>
|
||||||
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightTeal) ? 0.1 : 1}]} backgroundColor={colorMap.lightTeal}>
|
||||||
|
{selectedColor == colorMap.lightTeal && (
|
||||||
|
<AntDesign name="check" size={30} color="black" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.purple)} disabled={takenColors.includes(colorMap.purple)}>
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.purple) ? 0.1 : 1}]} backgroundColor={colorMap.purple}>
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.purple) ? 0.1 : 1}]} backgroundColor={colorMap.purple}>
|
||||||
{selectedColor == colorMap.purple && (
|
{selectedColor == colorMap.purple && (
|
||||||
<AntDesign name="check" size={30} color="white" />
|
<AntDesign name="check" size={30} color="white" />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightPurple)} disabled={takenColors.includes(colorMap.lightPurple)}>
|
||||||
<View row spread marginT-10>
|
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightPurple) ? 0.1 : 1}]} backgroundColor={colorMap.lightPurple}>
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.navy)} disabled={takenColors.includes(colorMap.navy)}>
|
{selectedColor == colorMap.lightPurple && (
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.navy) ? 0.1 : 1}]} backgroundColor={colorMap.navy}>
|
<AntDesign name="check" size={30} color="black" />
|
||||||
{selectedColor == colorMap.navy && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.red)} disabled={takenColors.includes(colorMap.red)}>
|
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.red) ? 0.1 : 1}]} backgroundColor={colorMap.red}>
|
|
||||||
{selectedColor == colorMap.red && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.indigo)} disabled={takenColors.includes(colorMap.indigo)}>
|
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.indigo) ? 0.1 : 1}]} backgroundColor={colorMap.indigo}>
|
|
||||||
{selectedColor == colorMap.indigo && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.emerald)} disabled={takenColors.includes(colorMap.emerald)}>
|
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.emerald) ? 0.1 : 1}]} backgroundColor={colorMap.emerald}>
|
|
||||||
{selectedColor == colorMap.emerald && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.violet)} disabled={takenColors.includes(colorMap.violet)}>
|
|
||||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.violet) ? 0.1 : 1}]} backgroundColor={colorMap.violet}>
|
|
||||||
{selectedColor == colorMap.violet && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@ -244,6 +244,15 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleChangeColor(colorMap.lightPink)}
|
||||||
|
>
|
||||||
|
<View style={styles.colorBox} backgroundColor={colorMap.lightPink}>
|
||||||
|
{selectedColor == colorMap.lightPink && (
|
||||||
|
<AntDesign name="check" size={30} color="white" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleChangeColor(colorMap.orange)}
|
onPress={() => handleChangeColor(colorMap.orange)}
|
||||||
>
|
>
|
||||||
@ -253,6 +262,15 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleChangeColor(colorMap.lightOrange)}
|
||||||
|
>
|
||||||
|
<View style={styles.colorBox} backgroundColor={colorMap.lightOrange}>
|
||||||
|
{selectedColor == colorMap.lightOrange && (
|
||||||
|
<AntDesign name="check" size={30} color="white" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleChangeColor(colorMap.green)}
|
onPress={() => handleChangeColor(colorMap.green)}
|
||||||
>
|
>
|
||||||
@ -262,15 +280,33 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View row spread marginT-10>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleChangeColor(colorMap.teal)}
|
onPress={() => handleChangeColor(colorMap.lightGreen)}
|
||||||
>
|
>
|
||||||
|
<View style={styles.colorBox} backgroundColor={colorMap.lightGreen}>
|
||||||
|
{selectedColor == colorMap.lightGreen && (
|
||||||
|
<AntDesign name="check" size={30} color="white" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.teal)}>
|
||||||
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
|
<View style={styles.colorBox} backgroundColor={colorMap.teal}>
|
||||||
{selectedColor == colorMap.teal && (
|
{selectedColor == colorMap.teal && (
|
||||||
<AntDesign name="check" size={30} color="white" />
|
<AntDesign name="check" size={30} color="white" />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleChangeColor(colorMap.lightTeal)}
|
||||||
|
>
|
||||||
|
<View style={styles.colorBox} backgroundColor={colorMap.lightTeal}>
|
||||||
|
{selectedColor == colorMap.lightTeal && (
|
||||||
|
<AntDesign name="check" size={30} color="white" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleChangeColor(colorMap.purple)}
|
onPress={() => handleChangeColor(colorMap.purple)}
|
||||||
>
|
>
|
||||||
@ -280,45 +316,9 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightPurple)}>
|
||||||
<View row spread marginT-10>
|
<View style={styles.colorBox} backgroundColor={colorMap.lightPurple}>
|
||||||
<TouchableOpacity
|
{selectedColor == colorMap.lightPurple && (
|
||||||
onPress={() => handleChangeColor(colorMap.navy)}
|
|
||||||
>
|
|
||||||
<View style={styles.colorBox} backgroundColor={colorMap.navy}>
|
|
||||||
{selectedColor == colorMap.navy && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.red)}>
|
|
||||||
<View style={styles.colorBox} backgroundColor={colorMap.red}>
|
|
||||||
{selectedColor == colorMap.red && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => handleChangeColor(colorMap.gray)}
|
|
||||||
>
|
|
||||||
<View style={styles.colorBox} backgroundColor={colorMap.gray}>
|
|
||||||
{selectedColor == colorMap.gray && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => handleChangeColor(colorMap.yellow)}
|
|
||||||
>
|
|
||||||
<View style={styles.colorBox} backgroundColor={colorMap.yellow}>
|
|
||||||
{selectedColor == colorMap.yellow && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.sky)}>
|
|
||||||
<View style={styles.colorBox} backgroundColor={colorMap.sky}>
|
|
||||||
{selectedColor == colorMap.sky && (
|
|
||||||
<AntDesign name="check" size={30} color="white" />
|
<AntDesign name="check" size={30} color="white" />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
export const colorMap = {
|
export const colorMap = {
|
||||||
pink: "#ea156c",
|
pink: "#ea156c",
|
||||||
orange: "#e28800",
|
orange: "#ffb902",
|
||||||
green: "#46a80a",
|
green: "#72d82a",
|
||||||
teal: "#05a8b6",
|
teal: "#07b9c6",
|
||||||
purple: "#7305d4",
|
purple: "#7204d5",
|
||||||
navy: '#002e42',
|
lightPink: '#ffc8dd',
|
||||||
red: '#ff1637',
|
lightOrange: '#ffe5a6',
|
||||||
gray: '#607d8b',
|
lightGreen: '#c1eea1',
|
||||||
yellow: '#ffc107',
|
lightTeal: '#c4f0f3',
|
||||||
sky: '#2196f3',
|
lightPurple: '#debffa',
|
||||||
indigo: '#4F46E5',
|
|
||||||
emerald: '#059669',
|
|
||||||
violet: '#7C3AED',
|
|
||||||
};
|
};
|
@ -9,7 +9,7 @@ import * as Notifications from 'expo-notifications';
|
|||||||
import * as Device from 'expo-device';
|
import * as Device from 'expo-device';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
import {Platform} from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import {useQueryClient} from "react-query";
|
import {useQueryClient} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
export enum ProfileType {
|
export enum ProfileType {
|
||||||
@ -166,7 +166,7 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNotification = async (notification: Notifications.Notification) => {
|
const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
queryClient.invalidateQueries(["notifications"]);
|
queryClient.invalidateQueries({queryKey: ["notifications"]});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
77
contexts/PersistQueryClientProvider.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { persistQueryClient } from '@tanstack/react-query-persist-client';
|
||||||
|
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import type { AsyncPersistRetryer } from '@tanstack/query-async-storage-persister';
|
||||||
|
|
||||||
|
const createQueryClient = () => new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
retry: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PersistQueryClientProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
|
const [isRestored, setIsRestored] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initPersistence = async () => {
|
||||||
|
const retry: AsyncPersistRetryer = async ({ persistedClient, error, errorCount }) => {
|
||||||
|
if (errorCount < 3) return persistedClient;
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const asyncStoragePersister = createAsyncStoragePersister({
|
||||||
|
storage: AsyncStorage,
|
||||||
|
key: 'REACT_QUERY_CACHE',
|
||||||
|
throttleTime: 1000,
|
||||||
|
serialize: data => JSON.stringify(data),
|
||||||
|
deserialize: str => JSON.parse(str),
|
||||||
|
retry
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
persistQueryClient({
|
||||||
|
queryClient,
|
||||||
|
persister: asyncStoragePersister,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
|
||||||
|
buster: 'v1',
|
||||||
|
dehydrateOptions: {
|
||||||
|
shouldDehydrateQuery: query => {
|
||||||
|
const persistedQueries = ['events'];
|
||||||
|
return persistedQueries.some(key =>
|
||||||
|
Array.isArray(query.queryKey) &&
|
||||||
|
query.queryKey.includes(key)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIsRestored(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore query cache:', error);
|
||||||
|
setIsRestored(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initPersistence();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
queryClient.clear();
|
||||||
|
};
|
||||||
|
}, [queryClient]);
|
||||||
|
|
||||||
|
if (!isRestored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
3
firebase/functions/app.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"expo": {}
|
||||||
|
}
|
@ -1144,12 +1144,14 @@ exports.cleanupTokenRefreshFlags = functions.pubsub
|
|||||||
|
|
||||||
async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId}) {
|
async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId}) {
|
||||||
const baseDate = new Date();
|
const baseDate = new Date();
|
||||||
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
const oneYearAgo = new Date(baseDate);
|
||||||
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||||
|
const oneYearAhead = new Date(baseDate);
|
||||||
|
oneYearAhead.setFullYear(oneYearAhead.getFullYear() + 1);
|
||||||
|
|
||||||
let totalEvents = 0;
|
let totalEvents = 0;
|
||||||
let pageToken = null;
|
let pageToken = null;
|
||||||
const batchSize = 50;
|
const batchSize = 250;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[FETCH] Starting event fetch for user: ${email}`);
|
console.log(`[FETCH] Starting event fetch for user: ${email}`);
|
||||||
@ -1158,12 +1160,12 @@ async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, c
|
|||||||
let events = [];
|
let events = [];
|
||||||
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
||||||
url.searchParams.set("singleEvents", "true");
|
url.searchParams.set("singleEvents", "true");
|
||||||
url.searchParams.set("timeMin", timeMin);
|
url.searchParams.set("timeMin", oneYearAgo.toISOString());
|
||||||
url.searchParams.set("timeMax", timeMax);
|
url.searchParams.set("timeMax", oneYearAhead.toISOString());
|
||||||
url.searchParams.set("maxResults", batchSize.toString());
|
url.searchParams.set("maxResults", batchSize.toString());
|
||||||
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
||||||
|
|
||||||
console.log(`[FETCH] Making request with token: ${token.substring(0, 10)}...`);
|
console.log(`[FETCH] Making request with token: ${token?.substring(0, 10)}...`);
|
||||||
|
|
||||||
let response = await fetch(url.toString(), {
|
let response = await fetch(url.toString(), {
|
||||||
headers: {
|
headers: {
|
||||||
@ -1178,12 +1180,10 @@ async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, c
|
|||||||
console.log(`[TOKEN] Token refreshed successfully during fetch`);
|
console.log(`[TOKEN] Token refreshed successfully during fetch`);
|
||||||
token = refreshedGoogleToken;
|
token = refreshedGoogleToken;
|
||||||
|
|
||||||
// Update token in Firestore
|
|
||||||
await db.collection("Profiles").doc(creatorId).update({
|
await db.collection("Profiles").doc(creatorId).update({
|
||||||
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
|
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
|
||||||
});
|
});
|
||||||
|
|
||||||
// Retry the request with new token
|
|
||||||
response = await fetch(url.toString(), {
|
response = await fetch(url.toString(), {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${refreshedGoogleToken}`,
|
Authorization: `Bearer ${refreshedGoogleToken}`,
|
||||||
|
@ -280,6 +280,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
||||||
|
|
||||||
|
"@react-native-async-storage/async-storage@1.23.1":
|
||||||
|
version "1.23.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz#cad3cd4fab7dacfe9838dce6ecb352f79150c883"
|
||||||
|
integrity sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==
|
||||||
|
dependencies:
|
||||||
|
merge-options "^3.0.4"
|
||||||
|
|
||||||
"@tootallnate/once@2":
|
"@tootallnate/once@2":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||||
@ -1470,6 +1477,11 @@ is-path-inside@^3.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||||
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
||||||
|
|
||||||
|
is-plain-obj@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
|
||||||
|
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
|
||||||
|
|
||||||
is-stream@^2.0.0:
|
is-stream@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
|
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
|
||||||
@ -1688,6 +1700,13 @@ merge-descriptors@1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
|
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
|
||||||
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
|
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
|
||||||
|
|
||||||
|
merge-options@^3.0.4:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
|
||||||
|
integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
|
||||||
|
dependencies:
|
||||||
|
is-plain-obj "^2.1.0"
|
||||||
|
|
||||||
merge2@^1.3.0:
|
merge2@^1.3.0:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import storage from "@react-native-firebase/storage";
|
import storage from "@react-native-firebase/storage";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
@ -53,7 +53,7 @@ export const useChangeProfilePicture = (customUserId?: string) => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Invalidate queries to refresh profile data
|
// Invalidate queries to refresh profile data
|
||||||
if (!customUserId) {
|
if (!customUserId) {
|
||||||
queryClient.invalidateQueries("Profiles");
|
queryClient.invalidateQueries({queryKey: ["Profiles"]});
|
||||||
refreshProfileData();
|
refreshProfileData();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {UserProfile} from "@firebase/auth";
|
import {UserProfile} from "@firebase/auth";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {EventData} from "@/hooks/firebase/types/eventData";
|
import {EventData} from "@/hooks/firebase/types/eventData";
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export const useCreateEvent = () => {
|
|||||||
.doc(docId)
|
.doc(docId)
|
||||||
.set({
|
.set({
|
||||||
...eventData,
|
...eventData,
|
||||||
attendees: (eventData.attendees?.length ?? 0) === 0 ?? [currentUser?.uid],
|
attendees: (eventData.attendees?.length ?? 0),
|
||||||
creatorId: currentUser?.uid,
|
creatorId: currentUser?.uid,
|
||||||
familyId: profileData?.familyId
|
familyId: profileData?.familyId
|
||||||
}, {merge: true});
|
}, {merge: true});
|
||||||
@ -83,7 +83,7 @@ export const useCreateEventsFromProvider = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("events");
|
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,47 +1,44 @@
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import {useMutation, useQueryClient} from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { IFeedback } from "@/contexts/FeedbackContext";
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
export const useCreateFeedback = () => {
|
export const useCreateFeedback = () => {
|
||||||
const {user: currentUser, profileData} = useAuthContext()
|
const { user: currentUser } = useAuthContext();
|
||||||
const queryClients = useQueryClient()
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["createFeedback"],
|
mutationKey: ["createFeedback"],
|
||||||
mutationFn: async (feedback: Partial<IFeedback>) => {
|
mutationFn: async (feedback: Partial<IFeedback>) => {
|
||||||
try {
|
if (feedback.id) {
|
||||||
if (feedback.id) {
|
const snapshot = await firestore()
|
||||||
const snapshot = await firestore()
|
|
||||||
.collection("Feedbacks")
|
|
||||||
.where("id", "==", feedback.id)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!snapshot.empty) {
|
|
||||||
const docId = snapshot.docs[0].id;
|
|
||||||
await firestore()
|
|
||||||
.collection("Feedbacks")
|
|
||||||
.doc(docId)
|
|
||||||
.set({
|
|
||||||
...feedback,
|
|
||||||
creatorId: currentUser?.uid,
|
|
||||||
}, {merge: true});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newDoc = firestore().collection('Feedbacks').doc();
|
|
||||||
await firestore()
|
|
||||||
.collection("Feedbacks")
|
.collection("Feedbacks")
|
||||||
.add({...feedback, id: newDoc.id, creatorId: currentUser?.uid});
|
.where("id", "==", feedback.id)
|
||||||
} catch (e) {
|
.get();
|
||||||
console.error(e);
|
|
||||||
|
if (!snapshot.empty) {
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.doc(docId)
|
||||||
|
.set({
|
||||||
|
...feedback,
|
||||||
|
creatorId: currentUser?.uid,
|
||||||
|
}, { merge: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newDoc = firestore().collection('Feedbacks').doc();
|
||||||
|
await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.add({ ...feedback, id: newDoc.id, creatorId: currentUser?.uid });
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClients.invalidateQueries("feedbacks")
|
queryClient.invalidateQueries({ queryKey: ["feedbacks"] });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useCreateFeedbacksFromProvider = () => {
|
export const useCreateFeedbacksFromProvider = () => {
|
||||||
const { user: currentUser } = useAuthContext();
|
const { user: currentUser } = useAuthContext();
|
||||||
@ -50,36 +47,29 @@ export const useCreateFeedbacksFromProvider = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["createFeedbacksFromProvider"],
|
mutationKey: ["createFeedbacksFromProvider"],
|
||||||
mutationFn: async (feedbackDataArray: Partial<IFeedback>[]) => {
|
mutationFn: async (feedbackDataArray: Partial<IFeedback>[]) => {
|
||||||
try {
|
const promises = feedbackDataArray.map(async (feedbackData) => {
|
||||||
const promises = feedbackDataArray.map(async (feedbackData) => {
|
const snapshot = await firestore()
|
||||||
console.log("Processing FeedbackData: ", feedbackData);
|
.collection("Feedbacks")
|
||||||
|
.where("id", "==", feedbackData.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
const snapshot = await firestore()
|
if (snapshot.empty) {
|
||||||
|
return firestore()
|
||||||
.collection("Feedbacks")
|
.collection("Feedbacks")
|
||||||
.where("id", "==", feedbackData.id)
|
.add({ ...feedbackData, creatorId: currentUser?.uid });
|
||||||
.get();
|
}
|
||||||
|
|
||||||
if (snapshot.empty) {
|
const docId = snapshot.docs[0].id;
|
||||||
return firestore()
|
return firestore()
|
||||||
.collection("Feedbacks")
|
.collection("Feedbacks")
|
||||||
.add({ ...feedbackData, creatorId: currentUser?.uid });
|
.doc(docId)
|
||||||
} else {
|
.set({ ...feedbackData, creatorId: currentUser?.uid }, { merge: true });
|
||||||
const docId = snapshot.docs[0].id;
|
});
|
||||||
return firestore()
|
|
||||||
.collection("Feedbacks")
|
|
||||||
.doc(docId)
|
|
||||||
.set({ ...feedbackData, creatorId: currentUser?.uid }, { merge: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error creating/updating feedbacks: ", e);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("feedbacks");
|
queryClient.invalidateQueries({ queryKey: ["feedbacks"] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,26 +1,30 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import {IGrocery} from "@/hooks/firebase/types/groceryData";
|
import { IGrocery } from "@/hooks/firebase/types/groceryData";
|
||||||
|
|
||||||
export const useCreateGrocery = () => {
|
export const useCreateGrocery = () => {
|
||||||
const { user: currentUser, profileData } = useAuthContext();
|
const { user: currentUser, profileData } = useAuthContext();
|
||||||
const queryClients = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const groceriesKey = ["groceries"];
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["createGrocery"],
|
mutationFn: (groceryData: Partial<IGrocery>) => {
|
||||||
mutationFn: async (groceryData: Partial<IGrocery>) => {
|
const newDoc = firestore().collection('Groceries').doc();
|
||||||
try {
|
return firestore()
|
||||||
const newDoc = firestore().collection('Groceries').doc();
|
.collection("Groceries")
|
||||||
await firestore()
|
.add({
|
||||||
.collection("Groceries")
|
...groceryData,
|
||||||
.add({...groceryData, id: newDoc.id, familyId: profileData?.familyId, creatorId: currentUser?.uid})
|
id: newDoc.id,
|
||||||
} catch (e) {
|
familyId: profileData?.familyId,
|
||||||
console.error(e)
|
creatorId: currentUser?.uid
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClients.invalidateQueries("groceries")
|
return queryClient.invalidateQueries({
|
||||||
|
queryKey: groceriesKey,
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { IFeedback } from "@/contexts/FeedbackContext";
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
import { IBrainDump } from "@/contexts/DumpContext";
|
import { IBrainDump } from "@/contexts/DumpContext";
|
||||||
@ -42,7 +42,7 @@ export const useCreateNote = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClients.invalidateQueries("braindumps");
|
queryClients.invalidateQueries({queryKey: ["braindumps"]});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -85,7 +85,7 @@ export const useCreateNotesFromProvider = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("braindumps");
|
queryClient.invalidateQueries({queryKey: ["braindumps"]});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
import functions from '@react-native-firebase/functions';
|
import functions from '@react-native-firebase/functions';
|
||||||
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { DAYS_OF_WEEK_ENUM, IToDo, REPEAT_TYPE } from "@/hooks/firebase/types/todoData";
|
import { DAYS_OF_WEEK_ENUM, IToDo, REPEAT_TYPE } from "@/hooks/firebase/types/todoData";
|
||||||
@ -141,7 +141,7 @@ export const useCreateTodo = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClients.invalidateQueries("todos")
|
queryClients.invalidateQueries({queryKey: ["todos"]})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
export const useDeleteEvent = () => {
|
export const useDeleteEvent = () => {
|
||||||
@ -33,7 +33,7 @@ export const useDeleteEvent = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("events");
|
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
export const useDeleteFeedback = () => {
|
export const useDeleteFeedback = () => {
|
||||||
@ -39,7 +39,7 @@ export const useDeleteFeedback = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("feedbacks");
|
queryClient.invalidateQueries({queryKey: ["feedbacks"]});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
export const useDeleteGrocery = () => {
|
export const useDeleteGrocery = () => {
|
||||||
@ -15,7 +15,7 @@ export const useDeleteGrocery = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("groceries");
|
queryClient.invalidateQueries({queryKey: ["groceries"]});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
export const useDeleteNote = () => {
|
export const useDeleteNote = () => {
|
||||||
@ -33,7 +33,7 @@ export const useDeleteNote = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("braindumps");
|
queryClient.invalidateQueries({queryKey: ["braindumps"]});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {Notification} from "@/hooks/firebase/useGetNotifications";
|
import { Notification } from "@/hooks/firebase/useGetNotifications";
|
||||||
|
|
||||||
export const useDeleteNotification = () => {
|
export const useDeleteNotification = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const {user} = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
const notificationsKey = ["notifications", user?.uid];
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: async (id: string) => {
|
||||||
@ -15,23 +16,24 @@ export const useDeleteNotification = () => {
|
|||||||
.delete();
|
.delete();
|
||||||
},
|
},
|
||||||
onMutate: async (deletedId) => {
|
onMutate: async (deletedId) => {
|
||||||
await queryClient.cancelQueries(["notifications", user?.uid]);
|
await queryClient.cancelQueries({ queryKey: notificationsKey });
|
||||||
|
|
||||||
const previousNotifications = queryClient.getQueryData<Notification[]>(["notifications", user?.uid]);
|
const previousNotifications = queryClient.getQueryData<Notification[]>(notificationsKey);
|
||||||
|
|
||||||
queryClient.setQueryData<Notification[]>(["notifications", user?.uid], (old) =>
|
queryClient.setQueryData<Notification[]>(
|
||||||
old?.filter((notification) => notification?.id! !== deletedId) ?? []
|
notificationsKey,
|
||||||
|
old => old?.filter(notification => notification?.id !== deletedId) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
return {previousNotifications};
|
return { previousNotifications };
|
||||||
},
|
},
|
||||||
onError: (_err, _deletedId, context) => {
|
onError: (_err, _deletedId, context) => {
|
||||||
if (context?.previousNotifications) {
|
if (context?.previousNotifications) {
|
||||||
queryClient.setQueryData(["notifications", user?.uid], context.previousNotifications);
|
queryClient.setQueryData(notificationsKey, context.previousNotifications);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(["notifications", user?.uid]);
|
queryClient.invalidateQueries({ queryKey: notificationsKey });
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import auth, {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
import auth, {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
export const useSignUp = () => {
|
export const useSignUp = () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useQuery} from "react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {ChildProfile} from "@/hooks/firebase/types/profileTypes";
|
import {ChildProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import {useQuery, useQueryClient} from "react-query";
|
import {useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useAtomValue} from "jotai";
|
import {useAtomValue} from "jotai";
|
||||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
||||||
import {colorMap} from "@/constants/colorMap";
|
import {colorMap} from "@/constants/colorMap";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {useEffect, useRef} from "react";
|
||||||
import {useEffect} from "react";
|
|
||||||
|
|
||||||
const createEventHash = (event: any): string => {
|
const createEventHash = (event: any): string => {
|
||||||
const str = `${event.startDate?.seconds || ''}-${event.endDate?.seconds || ''}-${
|
const str = `${event.startDate?.seconds || ''}-${event.endDate?.seconds || ''}-${
|
||||||
@ -21,214 +20,128 @@ const createEventHash = (event: any): string => {
|
|||||||
return hash.toString(36);
|
return hash.toString(36);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchEvents = async (userId: string, familyId: string | undefined, isFamilyView: boolean) => {
|
||||||
|
const db = firestore();
|
||||||
|
const eventsQuery = db.collection("Events");
|
||||||
|
let constraints = [];
|
||||||
|
|
||||||
|
if (isFamilyView) {
|
||||||
|
constraints = [
|
||||||
|
eventsQuery.where("familyId", "==", familyId).where("private", "==", false),
|
||||||
|
eventsQuery.where("creatorId", "==", userId),
|
||||||
|
eventsQuery.where("attendees", "array-contains", userId)
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
constraints = [
|
||||||
|
eventsQuery.where("creatorId", "==", userId),
|
||||||
|
eventsQuery.where("attendees", "array-contains", userId)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshots = await Promise.all(constraints.map(query => query.get()));
|
||||||
|
|
||||||
|
const uniqueEvents = new Map();
|
||||||
|
const processedHashes = new Set();
|
||||||
|
const creatorIds = new Set();
|
||||||
|
|
||||||
|
snapshots.forEach(snapshot => {
|
||||||
|
snapshot.docs.forEach(doc => {
|
||||||
|
const event = doc.data();
|
||||||
|
const hash = createEventHash(event);
|
||||||
|
|
||||||
|
if (!processedHashes.has(hash)) {
|
||||||
|
processedHashes.add(hash);
|
||||||
|
creatorIds.add(event.creatorId);
|
||||||
|
uniqueEvents.set(doc.id, event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const creatorIdsArray = Array.from(creatorIds);
|
||||||
|
const creatorProfiles = new Map();
|
||||||
|
|
||||||
|
for (let i = 0; i < creatorIdsArray.length; i += 10) {
|
||||||
|
const chunk = creatorIdsArray.slice(i, i + 10);
|
||||||
|
const profilesSnapshot = await db
|
||||||
|
.collection("Profiles")
|
||||||
|
.where(firestore.FieldPath.documentId(), "in", chunk)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
profilesSnapshot.docs.forEach(doc => {
|
||||||
|
creatorProfiles.set(doc.id, doc.data()?.eventColor || colorMap.pink);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(uniqueEvents.entries()).map(([id, event]) => ({
|
||||||
|
...event,
|
||||||
|
id,
|
||||||
|
start: event.allDay
|
||||||
|
? new Date(new Date(event.startDate.seconds * 1000).setHours(0, 0, 0, 0))
|
||||||
|
: new Date(event.startDate.seconds * 1000),
|
||||||
|
end: event.allDay
|
||||||
|
? new Date(new Date(event.endDate.seconds * 1000).setHours(0, 0, 0, 0))
|
||||||
|
: new Date(event.endDate.seconds * 1000),
|
||||||
|
hideHours: event.allDay,
|
||||||
|
eventColor: creatorProfiles.get(event.creatorId) || colorMap.pink,
|
||||||
|
notes: event.notes
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetEvents = () => {
|
export const useGetEvents = () => {
|
||||||
const {user, profileData} = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
const isFamilyView = useAtomValue(isFamilyViewAtom);
|
const isFamilyView = useAtomValue(isFamilyViewAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const lastSyncTimestamp = useRef<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!profileData?.familyId) {
|
if (!user?.uid || !profileData?.familyId) return;
|
||||||
console.log('[SYNC] No family ID available, skipping listener setup');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[SYNC] Setting up sync listener', {
|
const prefetchEvents = async () => {
|
||||||
familyId: profileData.familyId,
|
await queryClient.prefetchQuery({
|
||||||
userId: user?.uid,
|
queryKey: ["events", user.uid, false], // Personal events
|
||||||
isFamilyView
|
queryFn: () => fetchEvents(user.uid, profileData.familyId, false),
|
||||||
});
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryClient.prefetchQuery({
|
||||||
|
queryKey: ["events", user.uid, true], // Family events
|
||||||
|
queryFn: () => fetchEvents(user.uid, profileData.familyId, true),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
prefetchEvents();
|
||||||
|
|
||||||
const unsubscribe = firestore()
|
const unsubscribe = firestore()
|
||||||
.collection('Households')
|
.collection('Households')
|
||||||
.where("familyId", "==", profileData.familyId)
|
.where("familyId", "==", profileData.familyId)
|
||||||
.onSnapshot((snapshot) => {
|
.onSnapshot((snapshot) => {
|
||||||
console.log('[SYNC] Snapshot received', {
|
|
||||||
empty: snapshot.empty,
|
|
||||||
size: snapshot.size,
|
|
||||||
changes: snapshot.docChanges().length
|
|
||||||
});
|
|
||||||
|
|
||||||
snapshot.docChanges().forEach((change) => {
|
snapshot.docChanges().forEach((change) => {
|
||||||
console.log('[SYNC] Processing change', {
|
|
||||||
type: change.type,
|
|
||||||
docId: change.doc.id,
|
|
||||||
newData: change.doc.data()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (change.type === 'modified') {
|
if (change.type === 'modified') {
|
||||||
const data = change.doc.data();
|
const data = change.doc.data();
|
||||||
console.log('[SYNC] Modified document data', {
|
|
||||||
hasLastSyncTimestamp: !!data?.lastSyncTimestamp,
|
|
||||||
hasLastUpdateTimestamp: !!data?.lastUpdateTimestamp,
|
|
||||||
allFields: Object.keys(data || {})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data?.lastSyncTimestamp) {
|
if (data?.lastSyncTimestamp) {
|
||||||
console.log('[SYNC] Sync timestamp change detected', {
|
const newTimestamp = data.lastSyncTimestamp.seconds;
|
||||||
timestamp: data.lastSyncTimestamp.toDate(),
|
if (newTimestamp > lastSyncTimestamp.current) {
|
||||||
householdId: change.doc.id,
|
lastSyncTimestamp.current = newTimestamp;
|
||||||
queryKey: ["events", user?.uid, isFamilyView]
|
// Invalidate both queries
|
||||||
});
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["events", user.uid]
|
||||||
console.log('[SYNC] Invalidating queries...');
|
});
|
||||||
queryClient.invalidateQueries(["events", user?.uid, isFamilyView]);
|
}
|
||||||
console.log('[SYNC] Queries invalidated');
|
|
||||||
} else {
|
|
||||||
console.log('[SYNC] Modified document without lastSyncTimestamp', {
|
|
||||||
householdId: change.doc.id
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, (error) => {
|
}, console.error);
|
||||||
console.error('[SYNC] Listener error:', {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
stack: error.stack
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[SYNC] Listener setup complete');
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
console.log('[SYNC] Cleaning up sync listener', {
|
|
||||||
familyId: profileData.familyId,
|
|
||||||
userId: user?.uid
|
|
||||||
});
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [profileData?.familyId, user?.uid, isFamilyView, queryClient]);
|
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [profileData?.familyId, user?.uid, queryClient]);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["events", user?.uid, isFamilyView],
|
queryKey: ["events", user?.uid, isFamilyView],
|
||||||
queryFn: async () => {
|
queryFn: () => fetchEvents(user?.uid!, profileData?.familyId, isFamilyView),
|
||||||
console.log(`Fetching events - Family View: ${isFamilyView}, User: ${user?.uid}`);
|
|
||||||
|
|
||||||
const db = firestore();
|
|
||||||
const userId = user?.uid;
|
|
||||||
const familyId = profileData?.familyId;
|
|
||||||
let allEvents = [];
|
|
||||||
|
|
||||||
if (isFamilyView) {
|
|
||||||
const [publicFamilyEvents, privateCreatorEvents, privateAttendeeEvents, userAttendeeEvents, userCreatorEvents] = await Promise.all([
|
|
||||||
// Public family events
|
|
||||||
db.collection("Events")
|
|
||||||
.where("familyId", "==", familyId)
|
|
||||||
.where("private", "==", false)
|
|
||||||
.get(),
|
|
||||||
|
|
||||||
// Private events user created
|
|
||||||
db.collection("Events")
|
|
||||||
.where("familyId", "==", familyId)
|
|
||||||
.where("private", "==", true)
|
|
||||||
.where("creatorId", "==", userId)
|
|
||||||
.get(),
|
|
||||||
|
|
||||||
// Private events user is attending
|
|
||||||
db.collection("Events")
|
|
||||||
.where("private", "==", true)
|
|
||||||
.where("attendees", "array-contains", userId)
|
|
||||||
.get(),
|
|
||||||
|
|
||||||
// All events where user is attendee
|
|
||||||
db.collection("Events")
|
|
||||||
.where("attendees", "array-contains", userId)
|
|
||||||
.get(),
|
|
||||||
|
|
||||||
// ALL events where user is creator (regardless of attendees)
|
|
||||||
db.collection("Events")
|
|
||||||
.where("creatorId", "==", userId)
|
|
||||||
.get()
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`Found ${publicFamilyEvents.size} public events, ${privateCreatorEvents.size} private creator events, ${privateAttendeeEvents.size} private attendee events, ${userAttendeeEvents.size} user attendee events, ${userCreatorEvents.size} user creator events`);
|
|
||||||
|
|
||||||
allEvents = [
|
|
||||||
...publicFamilyEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
|
|
||||||
...privateCreatorEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
|
|
||||||
...privateAttendeeEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
|
|
||||||
...userAttendeeEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
|
|
||||||
...userCreatorEvents.docs.map(doc => ({...doc.data(), id: doc.id}))
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
const [creatorEvents, attendeeEvents] = await Promise.all([
|
|
||||||
db.collection("Events")
|
|
||||||
.where("creatorId", "==", userId)
|
|
||||||
.get(),
|
|
||||||
db.collection("Events")
|
|
||||||
.where("attendees", "array-contains", userId)
|
|
||||||
.get()
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`Found ${creatorEvents.size} creator events, ${attendeeEvents.size} attendee events`);
|
|
||||||
|
|
||||||
allEvents = [
|
|
||||||
...creatorEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
|
|
||||||
...attendeeEvents.docs.map(doc => ({...doc.data(), id: doc.id}))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueEventsMap = new Map();
|
|
||||||
const processedHashes = new Set();
|
|
||||||
|
|
||||||
allEvents.forEach(event => {
|
|
||||||
const eventHash = createEventHash(event);
|
|
||||||
|
|
||||||
console.log(`Processing ${uniqueEventsMap.size} unique events`);
|
|
||||||
|
|
||||||
const processedEvent = {
|
|
||||||
...event,
|
|
||||||
id: event.id || uuidv4(),
|
|
||||||
creatorId: event.creatorId || userId
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only add the event if we haven't seen this hash before
|
|
||||||
if (!processedHashes.has(eventHash)) {
|
|
||||||
processedHashes.add(eventHash);
|
|
||||||
uniqueEventsMap.set(processedEvent.id, processedEvent);
|
|
||||||
} else {
|
|
||||||
console.log(`Duplicate event detected and skipped using hash: ${eventHash}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Processing ${uniqueEventsMap.size} unique events after deduplication`);
|
|
||||||
|
|
||||||
const processedEvents = await Promise.all(
|
|
||||||
Array.from(uniqueEventsMap.values()).map(async (event) => {
|
|
||||||
const profileSnapshot = await db
|
|
||||||
.collection("Profiles")
|
|
||||||
.doc(event.creatorId)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
const profileData = profileSnapshot.data();
|
|
||||||
const eventColor = profileData?.eventColor || colorMap.pink;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...event,
|
|
||||||
start: event.allDay
|
|
||||||
? new Date(new Date(event.startDate.seconds * 1000).setHours(0, 0, 0, 0))
|
|
||||||
: new Date(event.startDate.seconds * 1000),
|
|
||||||
end: event.allDay
|
|
||||||
? new Date(new Date(event.endDate.seconds * 1000).setHours(0, 0, 0, 0))
|
|
||||||
: new Date(event.endDate.seconds * 1000),
|
|
||||||
hideHours: event.allDay,
|
|
||||||
eventColor,
|
|
||||||
notes: event.notes,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
console.log(`Events processing completed, returning ${processedEvents.length} events`);
|
|
||||||
return processedEvents;
|
|
||||||
},
|
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
cacheTime: 30 * 60 * 1000,
|
gcTime: Infinity,
|
||||||
keepPreviousData: true,
|
placeholderData: (previousData) => previousData,
|
||||||
onError: (error) => {
|
enabled: Boolean(user?.uid),
|
||||||
console.error('Error fetching events:', error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import {useQuery} from "react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { IFeedback } from "@/contexts/FeedbackContext";
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useQuery} from "react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
|
||||||
|
@ -1,36 +1,30 @@
|
|||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
export const useGetHouseholdName = (familyId: string) => {
|
export const useGetHouseholdName = (familyId: string) => {
|
||||||
return useQuery(
|
return useQuery({
|
||||||
["getHouseholdName", familyId], // Unique query key
|
queryKey: ["household", familyId],
|
||||||
async () => {
|
queryFn: async () => {
|
||||||
console.log(`Fetching household name for familyId: ${familyId}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Query the Households collection for the given familyId
|
|
||||||
const snapshot = await firestore()
|
const snapshot = await firestore()
|
||||||
.collection("Households")
|
.collection("Households")
|
||||||
.where("familyId", "==", familyId)
|
.where("familyId", "==", familyId)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (!snapshot.empty) {
|
if (!snapshot.empty) {
|
||||||
// Extract the name from the first matching document
|
|
||||||
const householdData = snapshot.docs[0].data();
|
const householdData = snapshot.docs[0].data();
|
||||||
console.log("Household found:", householdData);
|
console.log("Household found:", householdData);
|
||||||
return householdData.name || null; // Return the name or null if missing
|
return householdData.name ?? null;
|
||||||
} else {
|
|
||||||
console.log("No household found for the given familyId.");
|
|
||||||
return null; // Return null if no household found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("No household found for the given familyId.");
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error fetching household name:", e);
|
console.error("Error fetching household name:", e);
|
||||||
throw e; // Ensure error propagates to the query error handling
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
enabled: Boolean(familyId),
|
||||||
enabled: !!familyId, // Only fetch if familyId is provided
|
staleTime: 5 * 60 * 1000,
|
||||||
staleTime: 5 * 60 * 1000, // Cache the data for 5 minutes
|
});
|
||||||
}
|
};
|
||||||
);
|
|
||||||
};
|
|
@ -1,5 +1,5 @@
|
|||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { IBrainDump } from "@/contexts/DumpContext";
|
import { IBrainDump } from "@/contexts/DumpContext";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
@ -35,11 +35,11 @@ export const useGetNotifications = () => {
|
|||||||
const snapshot = await firestore()
|
const snapshot = await firestore()
|
||||||
.collection("Notifications")
|
.collection("Notifications")
|
||||||
.where("familyId", "==", profileData?.familyId)
|
.where("familyId", "==", profileData?.familyId)
|
||||||
|
.orderBy("timestamp", "desc")
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
return snapshot.docs.map((doc) => {
|
return snapshot.docs.map((doc) => {
|
||||||
const data = doc.data() as NotificationFirestore;
|
const data = doc.data() as NotificationFirestore;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...data,
|
...data,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import firestore, {or, query, where} from "@react-native-firebase/firestore";
|
import firestore, {or, query, where} from "@react-native-firebase/firestore";
|
||||||
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { IToDo } from "@/hooks/firebase/types/todoData";
|
import { IToDo } from "@/hooks/firebase/types/todoData";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import functions from '@react-native-firebase/functions';
|
import functions from '@react-native-firebase/functions';
|
||||||
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { HttpsCallableResult } from "@firebase/functions";
|
import { HttpsCallableResult } from "@firebase/functions";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
|
|
||||||
export const useResetPassword = () => {
|
export const useResetPassword = () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
|
|
||||||
export const useSignIn = () => {
|
export const useSignIn = () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
import {useRouter} from "expo-router";
|
import {useRouter} from "expo-router";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useSetUserData} from "./useSetUserData";
|
import {useSetUserData} from "./useSetUserData";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {EventData} from "@/hooks/firebase/types/eventData";
|
import {EventData} from "@/hooks/firebase/types/eventData";
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ export const useUpdateEvent = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClients.invalidateQueries("events")
|
queryClients.invalidateQueries({queryKey: ["events"]})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { IFeedback } from "@/contexts/FeedbackContext";
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ export const useUpdateFeedback = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (updatedFeedback) => {
|
onSuccess: (updatedFeedback) => {
|
||||||
queryClient.invalidateQueries("feedbacks");
|
queryClient.invalidateQueries({queryKey: ["feedbacks"]})
|
||||||
|
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
["feedback", updatedFeedback.id],
|
["feedback", updatedFeedback.id],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {IGrocery} from "@/hooks/firebase/types/groceryData";
|
import {IGrocery} from "@/hooks/firebase/types/groceryData";
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ export const useUpdateGrocery = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClients.invalidateQueries("groceries")
|
queryClients.invalidateQueries({queryKey: ["groceries"]})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const useUpdateHouseholdName = () => {
|
export const useUpdateHouseholdName = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -44,7 +44,7 @@ export const useUpdateHouseholdName = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("households"); // Invalidate the "households" query to refresh data
|
queryClient.invalidateQueries({queryKey: ["households"]}); // Invalidate the "households" query to refresh data
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { IBrainDump } from "@/contexts/DumpContext";
|
import { IBrainDump } from "@/contexts/DumpContext";
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ export const useUpdateNote = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (updatedNote) => {
|
onSuccess: (updatedNote) => {
|
||||||
queryClient.invalidateQueries("braindumps");
|
queryClient.invalidateQueries({queryKey: ["braindumps"]});
|
||||||
|
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
["feedback", updatedNote.id],
|
["feedback", updatedNote.id],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {IToDo, REPEAT_TYPE} from "@/hooks/firebase/types/todoData";
|
import {IToDo, REPEAT_TYPE} from "@/hooks/firebase/types/todoData";
|
||||||
import {addDays, addMonths, addWeeks, addYears, compareAsc, format, subDays} from "date-fns";
|
import {addDays, addMonths, addWeeks, addYears, compareAsc, format, subDays} from "date-fns";
|
||||||
@ -165,7 +165,7 @@ export const useUpdateTodo = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClients.invalidateQueries("todos")
|
queryClients.invalidateQueries({queryKey: ["todos"]})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ export const useUpdateUserData = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries("events");
|
queryClient.invalidateQueries({queryKey: ["events"]})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -9,7 +9,7 @@ import * as Google from "expo-auth-session/providers/google";
|
|||||||
import * as AuthSession from "expo-auth-session";
|
import * as AuthSession from "expo-auth-session";
|
||||||
import * as AppleAuthentication from "expo-apple-authentication";
|
import * as AppleAuthentication from "expo-apple-authentication";
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import {useQueryClient} from "react-query";
|
import {useQueryClient} from "@tanstack/react-query";
|
||||||
import {AppleAccount, GoogleAccount, MicrosoftAccount} from "@/hooks/firebase/types/profileTypes";
|
import {AppleAccount, GoogleAccount, MicrosoftAccount} from "@/hooks/firebase/types/profileTypes";
|
||||||
|
|
||||||
const googleConfig = {
|
const googleConfig = {
|
||||||
@ -302,7 +302,7 @@ export const useCalSync = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNotification = async (notification: Notifications.Notification) => {
|
const handleNotification = async (notification: Notifications.Notification) => {
|
||||||
queryClient.invalidateQueries(["events"]);
|
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
const sub = Notifications.addNotificationReceivedListener(handleNotification);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
||||||
import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils";
|
import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils";
|
||||||
@ -23,7 +23,6 @@ export const useFetchAndSaveAppleEvents = () => {
|
|||||||
timeMax
|
timeMax
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(response);
|
|
||||||
const items = response ?? [];
|
const items = response ?? [];
|
||||||
await createEventsFromProvider(items);
|
await createEventsFromProvider(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -32,7 +31,7 @@ export const useFetchAndSaveAppleEvents = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(["events"])
|
queryClient.invalidateQueries({queryKey: ["events"]})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import {useMutation, useQueryClient} from "react-query";
|
import {useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import functions from "@react-native-firebase/functions";
|
import functions from "@react-native-firebase/functions";
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export const useFetchAndSaveGoogleEvents = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries(["events"]);
|
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||||
console.log(`Successfully synced ${data.eventCount} events`);
|
console.log(`Successfully synced ${data.eventCount} events`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { useSetUserData } from "@/hooks/firebase/useSetUserData";
|
import { useSetUserData } from "@/hooks/firebase/useSetUserData";
|
||||||
import functions from '@react-native-firebase/functions';
|
import functions from '@react-native-firebase/functions';
|
||||||
@ -130,7 +130,7 @@ export const useFetchAndSaveMicrosoftEvents = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries(["events"]);
|
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||||
console.log(`Successfully synced ${data.eventCount} Microsoft events`);
|
console.log(`Successfully synced ${data.eventCount} Microsoft events`);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "Cally.app"
|
BuildableName = "Cally.app"
|
||||||
BlueprintName = "cally"
|
BlueprintName = "Cally"
|
||||||
ReferencedContainer = "container:cally.xcodeproj">
|
ReferencedContainer = "container:Cally.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
@ -33,9 +33,9 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||||
BuildableName = "callyTests.xctest"
|
BuildableName = "CallyTests.xctest"
|
||||||
BlueprintName = "callyTests"
|
BlueprintName = "CallyTests"
|
||||||
ReferencedContainer = "container:cally.xcodeproj">
|
ReferencedContainer = "container:Cally.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
@ -56,8 +56,8 @@
|
|||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "Cally.app"
|
BuildableName = "Cally.app"
|
||||||
BlueprintName = "cally"
|
BlueprintName = "Cally"
|
||||||
ReferencedContainer = "container:cally.xcodeproj">
|
ReferencedContainer = "container:Cally.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
@ -73,8 +73,8 @@
|
|||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "Cally.app"
|
BuildableName = "Cally.app"
|
||||||
BlueprintName = "cally"
|
BlueprintName = "Cally"
|
||||||
ReferencedContainer = "container:cally.xcodeproj">
|
ReferencedContainer = "container:Cally.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
@ -2,7 +2,7 @@
|
|||||||
<Workspace
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:cally.xcodeproj">
|
location = "group:Cally.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Pods/Pods.xcodeproj">
|
location = "group:Pods/Pods.xcodeproj">
|
@ -1,17 +1,18 @@
|
|||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import <Firebase/Firebase.h>
|
#import <Firebase/Firebase.h>
|
||||||
#import <Firebase.h>
|
|
||||||
|
|
||||||
#import <React/RCTBundleURLProvider.h>
|
#import <React/RCTBundleURLProvider.h>
|
||||||
#import <React/RCTLinkingManager.h>
|
#import <React/RCTLinkingManager.h>
|
||||||
#import "RNAppAuthAuthorizationFlowManager.h"
|
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||||
{
|
{
|
||||||
|
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
|
||||||
|
[FIRApp configure];
|
||||||
|
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
|
||||||
self.moduleName = @"main";
|
self.moduleName = @"main";
|
||||||
[FIRApp configure];
|
|
||||||
// You can add your custom initial props in the dictionary below.
|
// You can add your custom initial props in the dictionary below.
|
||||||
// They will be passed down to the ViewController used by React Native.
|
// They will be passed down to the ViewController used by React Native.
|
||||||
self.initialProps = @{};
|
self.initialProps = @{};
|