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
|
||||
import expo.modules.splashscreen.SplashScreenManager
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@ -15,7 +16,10 @@ class MainActivity : ReactActivity() {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
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>
|
||||
<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_status_bar_translucent" translatable="false">false</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:textColor">@android:color/black</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="android:windowBackground">@drawable/splashscreen_logo</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
</resources>
|
3
app.json
@ -4,6 +4,7 @@
|
||||
"slug": "cally",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"owner": "tomira",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "callyplanner",
|
||||
"userInterfaceStyle": "light",
|
||||
@ -16,7 +17,7 @@
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.cally.app",
|
||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||
"buildNumber": "74",
|
||||
"buildNumber": "100",
|
||||
"usesAppleSignIn": true,
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
|
@ -36,6 +36,7 @@ import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
||||
import { RouteProp } from "@react-navigation/core";
|
||||
import RefreshButton from "@/components/shared/RefreshButton";
|
||||
import { useCalSync } from "@/hooks/useCalSync";
|
||||
import {useIsFetching} from "@tanstack/react-query";
|
||||
|
||||
type DrawerParamList = {
|
||||
index: undefined;
|
||||
@ -77,6 +78,11 @@ export default function TabLayout() {
|
||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||
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 () => {
|
||||
try {
|
||||
await resyncAllCalendars();
|
||||
@ -116,7 +122,7 @@ export default function TabLayout() {
|
||||
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||
return isCalendarPage ? (
|
||||
<View marginR-16>
|
||||
<RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} />
|
||||
<RefreshButton onRefresh={onRefresh} isSyncing={isLoading} />
|
||||
</View>
|
||||
) : null;
|
||||
}
|
||||
@ -124,7 +130,7 @@ export default function TabLayout() {
|
||||
return (
|
||||
<View marginR-16 row centerV>
|
||||
{isCalendarPage && (
|
||||
<View marginR-16><RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} /></View>
|
||||
<View marginR-16><RefreshButton onRefresh={onRefresh} isSyncing={isLoading} /></View>
|
||||
)}
|
||||
<MemoizedViewSwitch navigation={navigation} />
|
||||
</View>
|
||||
|
@ -17,7 +17,12 @@ export default function Screen() {
|
||||
const {profileData} = useAuthContext()
|
||||
|
||||
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!
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
||||
@ -80,15 +85,8 @@ export default function Screen() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{flex: 1, height: "100%"}}
|
||||
contentContainerStyle={{flex: 1, height: "100%"}}
|
||||
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 "react-native-reanimated";
|
||||
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 {Platform} from 'react-native';
|
||||
import KeyboardManager from 'react-native-keyboard-manager';
|
||||
import {enableScreens} from 'react-native-screens';
|
||||
import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider";
|
||||
|
||||
enableScreens(true)
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
KeyboardManager.setEnable(true);
|
||||
KeyboardManager.setToolbarPreviousNextButtonEnable(true);
|
||||
@ -211,8 +209,6 @@ export default function RootLayout() {
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
|
||||
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
|
||||
(
|
||||
[
|
||||
@ -262,7 +258,7 @@ export default function RootLayout() {
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PersistQueryClientProvider>
|
||||
<AuthContextProvider>
|
||||
<ThemeProvider value={DefaultTheme}>
|
||||
<Stack>
|
||||
@ -273,6 +269,6 @@ export default function RootLayout() {
|
||||
<Toast/>
|
||||
</ThemeProvider>
|
||||
</AuthContextProvider>
|
||||
</QueryClientProvider>
|
||||
</PersistQueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,17 @@
|
||||
module.exports = function (api) {
|
||||
const env = process.env.NODE_ENV;
|
||||
api.cache(true);
|
||||
|
||||
let plugins = [];
|
||||
|
||||
if (env !== 'development') {
|
||||
plugins.push('transform-remove-console');
|
||||
}
|
||||
|
||||
return {
|
||||
presets: [
|
||||
'babel-preset-expo',
|
||||
]
|
||||
],
|
||||
plugins
|
||||
};
|
||||
};
|
@ -10,6 +10,7 @@ import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||
import {addMinutes, roundToNearestMinutes} from "date-fns";
|
||||
|
||||
export const AddEventDialog = () => {
|
||||
const [show, setShow] = useState(false);
|
||||
@ -20,7 +21,8 @@ export const AddEventDialog = () => {
|
||||
const handleOpenManualInputModal = () => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
setSelectedNewEndDate(new Date());
|
||||
const roundedDate = roundToNearestMinutes(new Date(), {nearestTo: 5});
|
||||
setSelectedNewEndDate(roundedDate);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
|
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 {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 React from 'react';
|
||||
import {StyleSheet, View, ActivityIndicator} from 'react-native';
|
||||
import {Text} from 'react-native-ui-lib';
|
||||
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
|
||||
import {useCalSync} from '@/hooks/useCalSync';
|
||||
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
|
||||
import {useAtom} 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"
|
||||
} from './atoms';
|
||||
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
|
||||
import {DetailedCalendar} from "@/components/pages/calendar/DetailedCalendar";
|
||||
|
||||
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 [];
|
||||
|
||||
// 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 (
|
||||
<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;
|
||||
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
({calendarHeight}) => {
|
||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
|
||||
const {data: events, isLoading} = useGetEvents();
|
||||
const {profileData, user} = useAuthContext();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
||||
const [mode] = useAtom(modeAtom);
|
||||
const {isSyncing} = useSyncEvents();
|
||||
useCalSync();
|
||||
|
||||
//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) => {
|
||||
if (isLoading || isSyncing) {
|
||||
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
|
||||
<View style={styles.loadingContainer}>
|
||||
{isSyncing && <Text>Syncing...</Text>}
|
||||
<ActivityIndicator size="large" color="#0000ff"/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
return mode === "month"
|
||||
? <MonthCalendar {...props} />
|
||||
: <DetailedCalendar {...props} />;
|
||||
});
|
||||
|
||||
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",
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
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,
|
||||
},
|
||||
});
|
@ -2,7 +2,7 @@ import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
Colors,
|
||||
DateTimePicker,
|
||||
DateTimePicker, Dialog,
|
||||
LoaderScreen,
|
||||
Modal,
|
||||
Picker,
|
||||
@ -39,8 +39,9 @@ import BinIcon from "@/assets/svgs/BinIcon";
|
||||
import DeleteEventDialog from "./DeleteEventDialog";
|
||||
import {useDeleteEvent} from "@/hooks/firebase/useDeleteEvent";
|
||||
import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
|
||||
import { addHours, startOfHour, startOfMinute } from "date-fns";
|
||||
import {addHours, format, startOfHour, startOfMinute} from "date-fns";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {Calendar} from "react-native-calendars";
|
||||
|
||||
const daysOfWeek = [
|
||||
{label: "Monday", value: "monday"},
|
||||
@ -84,6 +85,8 @@ export const ManuallyAddEventModal = () => {
|
||||
editEvent?.private || false
|
||||
);
|
||||
const [location, setLocation] = useState(editEvent?.location ?? "");
|
||||
const [showStartDatePicker, setShowStartDatePicker] = useState(false);
|
||||
const [showEndDatePicker, setShowEndDatePicker] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (allDayAtom === true) setIsAllDay(true);
|
||||
@ -110,7 +113,7 @@ export const ManuallyAddEventModal = () => {
|
||||
}
|
||||
|
||||
const baseDate = editEvent?.end ?? initialDate ?? new Date();
|
||||
return addHours(startOfHour(baseDate), 1);
|
||||
return addHours(baseDate, 1);
|
||||
});
|
||||
const [startDate, setStartDate] = useState(initialDate ?? new Date());
|
||||
const [endDate, setEndDate] = useState(
|
||||
@ -159,7 +162,7 @@ export const ManuallyAddEventModal = () => {
|
||||
}
|
||||
|
||||
const baseDate = editEvent?.end ?? initialDate ?? new Date();
|
||||
return addHours(startOfHour(baseDate), 1);
|
||||
return addHours(baseDate, 1);
|
||||
});
|
||||
|
||||
setStartDate(initialDate ?? new Date());
|
||||
@ -305,7 +308,172 @@ export const ManuallyAddEventModal = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const renderCalendarPicker = (
|
||||
isStart: boolean,
|
||||
visible: boolean,
|
||||
onDismiss: () => void
|
||||
) => {
|
||||
const currentDate = isStart ? startDate : endDate;
|
||||
const setDate = isStart ? setStartDate : setEndDate;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
panDirection={"down"}
|
||||
width="100%"
|
||||
bottom
|
||||
|
||||
containerStyle={{
|
||||
backgroundColor: Colors.white,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
<View padding-20>
|
||||
<View row spread marginB-20>
|
||||
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
|
||||
Select Date
|
||||
</Text>
|
||||
<TouchableOpacity onPress={onDismiss}>
|
||||
<Text style={{color: Colors.$textPrimary}}>Done</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Calendar
|
||||
firstDay={1}
|
||||
current={format(currentDate, 'yyyy-MM-dd')}
|
||||
minDate={isStart ? undefined : format(startDate, 'yyyy-MM-dd')}
|
||||
markedDates={{
|
||||
[format(currentDate, 'yyyy-MM-dd')]: {
|
||||
selected: true,
|
||||
selectedColor: '#ea156c'
|
||||
}
|
||||
}}
|
||||
onDayPress={(day) => {
|
||||
const newDate = new Date(day.timestamp);
|
||||
newDate.setHours(currentDate.getHours());
|
||||
newDate.setMinutes(currentDate.getMinutes());
|
||||
setDate(newDate);
|
||||
|
||||
if (isStart && newDate > endDate) {
|
||||
const newEndDate = new Date(newDate);
|
||||
newEndDate.setHours(endDate.getHours());
|
||||
newEndDate.setMinutes(endDate.getMinutes());
|
||||
setEndDate(newEndDate);
|
||||
}
|
||||
|
||||
onDismiss();
|
||||
}}
|
||||
theme={{
|
||||
selectedDayBackgroundColor: '#ea156c',
|
||||
selectedDayTextColor: '#ffffff',
|
||||
todayTextColor: '#ea156c',
|
||||
arrowColor: '#ea156c',
|
||||
}}
|
||||
enableSwipeMonths={true}
|
||||
/>
|
||||
</View>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDateSection = () => (
|
||||
<View marginL-30 centerV>
|
||||
<View row spread marginB-10 centerV>
|
||||
<View row>
|
||||
<AntDesign name="clockcircleo" size={24} color="#919191"/>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
}}
|
||||
marginL-10
|
||||
>
|
||||
All day
|
||||
</Text>
|
||||
</View>
|
||||
<View right marginR-30>
|
||||
<Switch
|
||||
onColor={"#ea156c"}
|
||||
offColor={"#e1e1e2"}
|
||||
marginL-10
|
||||
value={isAllDay}
|
||||
onValueChange={(value) => setIsAllDay(value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View row marginB-10 spread centerV>
|
||||
<View row centerV>
|
||||
<Feather name="calendar" size={25} color="#919191"/>
|
||||
<TouchableOpacity onPress={() => setShowStartDatePicker(true)}>
|
||||
<Text marginL-8 style={styles.dateText}>
|
||||
{format(startDate, 'MMM d, yyyy')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{!isAllDay && (
|
||||
<View right marginR-30>
|
||||
<DateTimePicker
|
||||
value={startTime}
|
||||
onChange={(time) => {
|
||||
if (time <= endTime) {
|
||||
setStartTime(time);
|
||||
}
|
||||
}}
|
||||
minuteInterval={5}
|
||||
mode="time"
|
||||
timeFormat="HH:mm"
|
||||
style={[styles.timePicker]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{!isAllDay && (
|
||||
<View row marginB-10 spread centerV>
|
||||
<View row centerV>
|
||||
<Feather name="calendar" size={25} color="#919191"/>
|
||||
<TouchableOpacity onPress={() => setShowEndDatePicker(true)}>
|
||||
<Text marginL-8 style={styles.dateText}>
|
||||
{format(endDate, 'MMM d, yyyy')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View right marginR-30>
|
||||
<DateTimePicker
|
||||
value={endTime}
|
||||
onChange={(time) => {
|
||||
setEndTime(time);
|
||||
if (
|
||||
endDate.getDate() === startDate.getDate() &&
|
||||
time.getHours() < startTime.getHours()
|
||||
) {
|
||||
const newEndDate = new Date(endDate);
|
||||
newEndDate.setDate(newEndDate.getDate() + 1);
|
||||
setEndDate(newEndDate);
|
||||
}
|
||||
}}
|
||||
minuteInterval={5}
|
||||
mode="time"
|
||||
timeFormat="HH:mm"
|
||||
style={[styles.timePicker]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{renderCalendarPicker(true, showStartDatePicker, () => setShowStartDatePicker(false))}
|
||||
{renderCalendarPicker(false, showEndDatePicker, () => setShowEndDatePicker(false))}
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={show}
|
||||
animationType="slide"
|
||||
@ -374,9 +542,8 @@ export const ManuallyAddEventModal = () => {
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View row center>
|
||||
<DropModalIcon onPress={close} />
|
||||
</View>
|
||||
|
||||
|
||||
<View flexS row gap-10>
|
||||
<TouchableOpacity onPress={handleSave}>
|
||||
<Text
|
||||
@ -416,130 +583,7 @@ export const ManuallyAddEventModal = () => {
|
||||
returnKeyType="next"
|
||||
/>
|
||||
<View style={styles.divider} marginT-8/>
|
||||
<View marginL-30 centerV>
|
||||
<View row spread marginB-10 centerV>
|
||||
<View row>
|
||||
<AntDesign name="clockcircleo" size={24} color="#919191" />
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
}}
|
||||
marginL-10
|
||||
>
|
||||
All day
|
||||
</Text>
|
||||
</View>
|
||||
<View right marginR-30>
|
||||
<Switch
|
||||
onColor={"#ea156c"}
|
||||
offColor={"#e1e1e2"}
|
||||
marginL-10
|
||||
value={isAllDay}
|
||||
onValueChange={(value) => setIsAllDay(value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View row marginB-10 spread>
|
||||
<View row centerV>
|
||||
<Feather name="calendar" size={25} color="#919191" />
|
||||
<DateTimePicker
|
||||
value={startDate}
|
||||
onChange={(date) => {
|
||||
setStartDate(date);
|
||||
|
||||
if (date > endDate) {
|
||||
const newEndDate = new Date(date);
|
||||
newEndDate.setDate(date.getDate() + 1);
|
||||
setEndDate(newEndDate);
|
||||
}
|
||||
}}
|
||||
//maximumDate={endDate}
|
||||
style={{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
}}
|
||||
marginL-8
|
||||
/>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
value={startTime}
|
||||
onChange={(time) => {
|
||||
if (
|
||||
endDate.getDate() === startDate.getDate() &&
|
||||
time.getHours() < endTime.getHours()
|
||||
){
|
||||
setStartTime(time);
|
||||
console.log('should happen')
|
||||
}
|
||||
}}
|
||||
maximumDate={endTime}
|
||||
minuteInterval={5}
|
||||
dateTimeFormatter={(date, mode) =>
|
||||
date.toLocaleTimeString("en-us", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
}
|
||||
mode="time"
|
||||
style={{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
}}
|
||||
marginR-30
|
||||
/>
|
||||
</View>
|
||||
|
||||
{!isAllDay && (
|
||||
<View row marginB-10 spread>
|
||||
<View row centerV>
|
||||
<Feather name="calendar" size={25} color="#919191" />
|
||||
<DateTimePicker
|
||||
value={endDate}
|
||||
minimumDate={startDate}
|
||||
text70
|
||||
marginL-8
|
||||
onChange={(date) => {
|
||||
setEndDate(date);
|
||||
}}
|
||||
style={{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
value={endTime}
|
||||
onChange={(time) => {
|
||||
setEndTime(time);
|
||||
if (
|
||||
endDate.getDate() === startDate.getDate() &&
|
||||
time.getHours() < startTime.getHours()
|
||||
) {
|
||||
const newEndDate = new Date(endDate);
|
||||
newEndDate.setDate(newEndDate.getDate() + 1);
|
||||
setEndDate(newEndDate);
|
||||
|
||||
console.log('new day');
|
||||
}
|
||||
}}
|
||||
minuteInterval={5}
|
||||
dateTimeFormatter={(date, mode) =>
|
||||
date.toLocaleTimeString("en-us", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
}
|
||||
mode="time"
|
||||
style={{
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
}}
|
||||
marginR-30
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{renderDateSection()}
|
||||
|
||||
<View style={styles.divider}/>
|
||||
|
||||
@ -756,6 +800,7 @@ export const ManuallyAddEventModal = () => {
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -785,4 +830,15 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 10,
|
||||
marginTop: 25,
|
||||
},
|
||||
timePicker: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
},
|
||||
dateText: {
|
||||
fontFamily: "PlusJakartaSans_500Medium",
|
||||
fontSize: 16,
|
||||
},
|
||||
dateButton: {
|
||||
marginTop: 10,
|
||||
}
|
||||
});
|
||||
|
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 * as Device from "expo-device";
|
||||
import { DeviceType } from "expo-device";
|
||||
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||
|
||||
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
|
||||
|
||||
@ -38,6 +39,7 @@ const SignUpPage = () => {
|
||||
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
||||
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
||||
const { mutateAsync: signUp, isLoading } = useSignUp();
|
||||
const {profileData} = useAuthContext();
|
||||
|
||||
const lnameRef = useRef<TextFieldRef>(null);
|
||||
const emailRef = useRef<TextFieldRef>(null);
|
||||
@ -68,7 +70,12 @@ const SignUpPage = () => {
|
||||
|
||||
const handleSignUp = async () => {
|
||||
await signUp({ email, password, firstName, lastName });
|
||||
|
||||
if (profileData?.userType === ProfileType.FAMILY_DEVICE) {
|
||||
router.replace("/(auth)/calendar");
|
||||
} else {
|
||||
router.replace("/(unauth)/cal_sync");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -291,6 +291,13 @@ const MyProfile = () => {
|
||||
)}
|
||||
</View>
|
||||
</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)}>
|
||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.orange) ? 0.1 : 1}]} backgroundColor={colorMap.orange}>
|
||||
{selectedColor == colorMap.orange && (
|
||||
@ -298,6 +305,13 @@ const MyProfile = () => {
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<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}>
|
||||
{selectedColor == colorMap.green && (
|
||||
@ -305,6 +319,15 @@ const MyProfile = () => {
|
||||
)}
|
||||
</View>
|
||||
</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)}>
|
||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.teal) ? 0.1 : 1}]} backgroundColor={colorMap.teal}>
|
||||
{selectedColor == colorMap.teal && (
|
||||
@ -312,6 +335,13 @@ const MyProfile = () => {
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<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}>
|
||||
{selectedColor == colorMap.purple && (
|
||||
@ -319,40 +349,10 @@ const MyProfile = () => {
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View row spread marginT-10>
|
||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.navy)} disabled={takenColors.includes(colorMap.navy)}>
|
||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.navy) ? 0.1 : 1}]} backgroundColor={colorMap.navy}>
|
||||
{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" />
|
||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightPurple)} disabled={takenColors.includes(colorMap.lightPurple)}>
|
||||
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightPurple) ? 0.1 : 1}]} backgroundColor={colorMap.lightPurple}>
|
||||
{selectedColor == colorMap.lightPurple && (
|
||||
<AntDesign name="check" size={30} color="black" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
@ -244,6 +244,15 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
||||
)}
|
||||
</View>
|
||||
</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
|
||||
onPress={() => handleChangeColor(colorMap.orange)}
|
||||
>
|
||||
@ -253,6 +262,15 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
||||
)}
|
||||
</View>
|
||||
</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
|
||||
onPress={() => handleChangeColor(colorMap.green)}
|
||||
>
|
||||
@ -262,15 +280,33 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View row spread marginT-10>
|
||||
<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}>
|
||||
{selectedColor == colorMap.teal && (
|
||||
<AntDesign name="check" size={30} color="white" />
|
||||
)}
|
||||
</View>
|
||||
</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
|
||||
onPress={() => handleChangeColor(colorMap.purple)}
|
||||
>
|
||||
@ -280,45 +316,9 @@ const UpdateUserDialog = ({ open, handleClose, profileData }: Props) => {
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View row spread marginT-10>
|
||||
<TouchableOpacity
|
||||
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 && (
|
||||
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightPurple)}>
|
||||
<View style={styles.colorBox} backgroundColor={colorMap.lightPurple}>
|
||||
{selectedColor == colorMap.lightPurple && (
|
||||
<AntDesign name="check" size={30} color="white" />
|
||||
)}
|
||||
</View>
|
||||
|
@ -1,15 +1,12 @@
|
||||
export const colorMap = {
|
||||
pink: "#ea156c",
|
||||
orange: "#e28800",
|
||||
green: "#46a80a",
|
||||
teal: "#05a8b6",
|
||||
purple: "#7305d4",
|
||||
navy: '#002e42',
|
||||
red: '#ff1637',
|
||||
gray: '#607d8b',
|
||||
yellow: '#ffc107',
|
||||
sky: '#2196f3',
|
||||
indigo: '#4F46E5',
|
||||
emerald: '#059669',
|
||||
violet: '#7C3AED',
|
||||
orange: "#ffb902",
|
||||
green: "#72d82a",
|
||||
teal: "#07b9c6",
|
||||
purple: "#7204d5",
|
||||
lightPink: '#ffc8dd',
|
||||
lightOrange: '#ffe5a6',
|
||||
lightGreen: '#c1eea1',
|
||||
lightTeal: '#c4f0f3',
|
||||
lightPurple: '#debffa',
|
||||
};
|
@ -9,7 +9,7 @@ import * as Notifications from 'expo-notifications';
|
||||
import * as Device from 'expo-device';
|
||||
import Constants from 'expo-constants';
|
||||
import {Platform} from 'react-native';
|
||||
import {useQueryClient} from "react-query";
|
||||
import {useQueryClient} from "@tanstack/react-query";
|
||||
|
||||
|
||||
export enum ProfileType {
|
||||
@ -166,7 +166,7 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
||||
|
||||
useEffect(() => {
|
||||
const handleNotification = async (notification: Notifications.Notification) => {
|
||||
queryClient.invalidateQueries(["notifications"]);
|
||||
queryClient.invalidateQueries({queryKey: ["notifications"]});
|
||||
};
|
||||
|
||||
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}) {
|
||||
const baseDate = new Date();
|
||||
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||
const oneYearAgo = new Date(baseDate);
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
const oneYearAhead = new Date(baseDate);
|
||||
oneYearAhead.setFullYear(oneYearAhead.getFullYear() + 1);
|
||||
|
||||
let totalEvents = 0;
|
||||
let pageToken = null;
|
||||
const batchSize = 50;
|
||||
const batchSize = 250;
|
||||
|
||||
try {
|
||||
console.log(`[FETCH] Starting event fetch for user: ${email}`);
|
||||
@ -1158,12 +1160,12 @@ async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, c
|
||||
let events = [];
|
||||
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
|
||||
url.searchParams.set("singleEvents", "true");
|
||||
url.searchParams.set("timeMin", timeMin);
|
||||
url.searchParams.set("timeMax", timeMax);
|
||||
url.searchParams.set("timeMin", oneYearAgo.toISOString());
|
||||
url.searchParams.set("timeMax", oneYearAhead.toISOString());
|
||||
url.searchParams.set("maxResults", batchSize.toString());
|
||||
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(), {
|
||||
headers: {
|
||||
@ -1178,12 +1180,10 @@ async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, c
|
||||
console.log(`[TOKEN] Token refreshed successfully during fetch`);
|
||||
token = refreshedGoogleToken;
|
||||
|
||||
// Update token in Firestore
|
||||
await db.collection("Profiles").doc(creatorId).update({
|
||||
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
|
||||
});
|
||||
|
||||
// Retry the request with new token
|
||||
response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${refreshedGoogleToken}`,
|
||||
|
@ -280,6 +280,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||
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":
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.4.1"
|
||||
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 storage from "@react-native-firebase/storage";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
@ -53,7 +53,7 @@ export const useChangeProfilePicture = (customUserId?: string) => {
|
||||
onSuccess: () => {
|
||||
// Invalidate queries to refresh profile data
|
||||
if (!customUserId) {
|
||||
queryClient.invalidateQueries("Profiles");
|
||||
queryClient.invalidateQueries({queryKey: ["Profiles"]});
|
||||
refreshProfileData();
|
||||
}
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {useMutation} from "react-query";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {UserProfile} from "@firebase/auth";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {EventData} from "@/hooks/firebase/types/eventData";
|
||||
|
||||
@ -24,7 +24,7 @@ export const useCreateEvent = () => {
|
||||
.doc(docId)
|
||||
.set({
|
||||
...eventData,
|
||||
attendees: (eventData.attendees?.length ?? 0) === 0 ?? [currentUser?.uid],
|
||||
attendees: (eventData.attendees?.length ?? 0),
|
||||
creatorId: currentUser?.uid,
|
||||
familyId: profileData?.familyId
|
||||
}, {merge: true});
|
||||
@ -83,7 +83,7 @@ export const useCreateEventsFromProvider = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries("events");
|
||||
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||
}
|
||||
});
|
||||
};
|
@ -1,16 +1,15 @@
|
||||
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 { IFeedback } from "@/contexts/FeedbackContext";
|
||||
|
||||
export const useCreateFeedback = () => {
|
||||
const {user: currentUser, profileData} = useAuthContext()
|
||||
const queryClients = useQueryClient()
|
||||
const { user: currentUser } = useAuthContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["createFeedback"],
|
||||
mutationFn: async (feedback: Partial<IFeedback>) => {
|
||||
try {
|
||||
if (feedback.id) {
|
||||
const snapshot = await firestore()
|
||||
.collection("Feedbacks")
|
||||
@ -29,19 +28,17 @@ export const useCreateFeedback = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newDoc = firestore().collection('Feedbacks').doc();
|
||||
await firestore()
|
||||
.collection("Feedbacks")
|
||||
.add({ ...feedback, id: newDoc.id, creatorId: currentUser?.uid });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClients.invalidateQueries("feedbacks")
|
||||
}
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: ["feedbacks"] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateFeedbacksFromProvider = () => {
|
||||
const { user: currentUser } = useAuthContext();
|
||||
@ -50,10 +47,7 @@ export const useCreateFeedbacksFromProvider = () => {
|
||||
return useMutation({
|
||||
mutationKey: ["createFeedbacksFromProvider"],
|
||||
mutationFn: async (feedbackDataArray: Partial<IFeedback>[]) => {
|
||||
try {
|
||||
const promises = feedbackDataArray.map(async (feedbackData) => {
|
||||
console.log("Processing FeedbackData: ", feedbackData);
|
||||
|
||||
const snapshot = await firestore()
|
||||
.collection("Feedbacks")
|
||||
.where("id", "==", feedbackData.id)
|
||||
@ -63,23 +57,19 @@ export const useCreateFeedbacksFromProvider = () => {
|
||||
return firestore()
|
||||
.collection("Feedbacks")
|
||||
.add({ ...feedbackData, creatorId: currentUser?.uid });
|
||||
} else {
|
||||
}
|
||||
|
||||
const docId = snapshot.docs[0].id;
|
||||
return firestore()
|
||||
.collection("Feedbacks")
|
||||
.doc(docId)
|
||||
.set({ ...feedbackData, creatorId: currentUser?.uid }, { merge: true });
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Error creating/updating feedbacks: ", e);
|
||||
}
|
||||
},
|
||||
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 { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { IGrocery } from "@/hooks/firebase/types/groceryData";
|
||||
|
||||
export const useCreateGrocery = () => {
|
||||
const { user: currentUser, profileData } = useAuthContext();
|
||||
const queryClients = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
const groceriesKey = ["groceries"];
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["createGrocery"],
|
||||
mutationFn: async (groceryData: Partial<IGrocery>) => {
|
||||
try {
|
||||
mutationFn: (groceryData: Partial<IGrocery>) => {
|
||||
const newDoc = firestore().collection('Groceries').doc();
|
||||
await firestore()
|
||||
return firestore()
|
||||
.collection("Groceries")
|
||||
.add({...groceryData, id: newDoc.id, familyId: profileData?.familyId, creatorId: currentUser?.uid})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
.add({
|
||||
...groceryData,
|
||||
id: newDoc.id,
|
||||
familyId: profileData?.familyId,
|
||||
creatorId: currentUser?.uid
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClients.invalidateQueries("groceries")
|
||||
}
|
||||
})
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: groceriesKey,
|
||||
exact: true
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
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 { IFeedback } from "@/contexts/FeedbackContext";
|
||||
import { IBrainDump } from "@/contexts/DumpContext";
|
||||
@ -42,7 +42,7 @@ export const useCreateNote = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClients.invalidateQueries("braindumps");
|
||||
queryClients.invalidateQueries({queryKey: ["braindumps"]});
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -85,7 +85,7 @@ export const useCreateNotesFromProvider = () => {
|
||||
}
|
||||
},
|
||||
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 functions from '@react-native-firebase/functions';
|
||||
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 { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { DAYS_OF_WEEK_ENUM, IToDo, REPEAT_TYPE } from "@/hooks/firebase/types/todoData";
|
||||
@ -141,7 +141,7 @@ export const useCreateTodo = () => {
|
||||
}
|
||||
},
|
||||
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";
|
||||
|
||||
export const useDeleteEvent = () => {
|
||||
@ -33,7 +33,7 @@ export const useDeleteEvent = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries("events");
|
||||
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||
}
|
||||
});
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
export const useDeleteFeedback = () => {
|
||||
@ -39,7 +39,7 @@ export const useDeleteFeedback = () => {
|
||||
}
|
||||
},
|
||||
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";
|
||||
|
||||
export const useDeleteGrocery = () => {
|
||||
@ -15,7 +15,7 @@ export const useDeleteGrocery = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries("groceries");
|
||||
queryClient.invalidateQueries({queryKey: ["groceries"]});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
export const useDeleteNote = () => {
|
||||
@ -33,7 +33,7 @@ export const useDeleteNote = () => {
|
||||
}
|
||||
},
|
||||
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 { useAuthContext } from "@/contexts/AuthContext";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import { Notification } from "@/hooks/firebase/useGetNotifications";
|
||||
@ -6,6 +6,7 @@ import {Notification} from "@/hooks/firebase/useGetNotifications";
|
||||
export const useDeleteNotification = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuthContext();
|
||||
const notificationsKey = ["notifications", user?.uid];
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
@ -15,23 +16,24 @@ export const useDeleteNotification = () => {
|
||||
.delete();
|
||||
},
|
||||
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) =>
|
||||
old?.filter((notification) => notification?.id! !== deletedId) ?? []
|
||||
queryClient.setQueryData<Notification[]>(
|
||||
notificationsKey,
|
||||
old => old?.filter(notification => notification?.id !== deletedId) ?? []
|
||||
);
|
||||
|
||||
return { previousNotifications };
|
||||
},
|
||||
onError: (_err, _deletedId, context) => {
|
||||
if (context?.previousNotifications) {
|
||||
queryClient.setQueryData(["notifications", user?.uid], context.previousNotifications);
|
||||
queryClient.setQueryData(notificationsKey, context.previousNotifications);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(["notifications", user?.uid]);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: notificationsKey });
|
||||
}
|
||||
});
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useMutation} from "react-query";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import auth, {FirebaseAuthTypes} from "@react-native-firebase/auth";
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useMutation} from "react-query";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||
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";
|
||||
|
||||
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 firestore from "@react-native-firebase/firestore";
|
||||
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 {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
||||
import {colorMap} from "@/constants/colorMap";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
import {useEffect} from "react";
|
||||
import {useEffect, useRef} from "react";
|
||||
|
||||
const createEventHash = (event: any): string => {
|
||||
const str = `${event.startDate?.seconds || ''}-${event.endDate?.seconds || ''}-${
|
||||
@ -21,192 +20,61 @@ const createEventHash = (event: any): string => {
|
||||
return hash.toString(36);
|
||||
};
|
||||
|
||||
|
||||
export const useGetEvents = () => {
|
||||
const {user, profileData} = useAuthContext();
|
||||
const isFamilyView = useAtomValue(isFamilyViewAtom);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!profileData?.familyId) {
|
||||
console.log('[SYNC] No family ID available, skipping listener setup');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[SYNC] Setting up sync listener', {
|
||||
familyId: profileData.familyId,
|
||||
userId: user?.uid,
|
||||
isFamilyView
|
||||
});
|
||||
|
||||
const unsubscribe = firestore()
|
||||
.collection('Households')
|
||||
.where("familyId", "==", profileData.familyId)
|
||||
.onSnapshot((snapshot) => {
|
||||
console.log('[SYNC] Snapshot received', {
|
||||
empty: snapshot.empty,
|
||||
size: snapshot.size,
|
||||
changes: snapshot.docChanges().length
|
||||
});
|
||||
|
||||
snapshot.docChanges().forEach((change) => {
|
||||
console.log('[SYNC] Processing change', {
|
||||
type: change.type,
|
||||
docId: change.doc.id,
|
||||
newData: change.doc.data()
|
||||
});
|
||||
|
||||
if (change.type === 'modified') {
|
||||
const data = change.doc.data();
|
||||
console.log('[SYNC] Modified document data', {
|
||||
hasLastSyncTimestamp: !!data?.lastSyncTimestamp,
|
||||
hasLastUpdateTimestamp: !!data?.lastUpdateTimestamp,
|
||||
allFields: Object.keys(data || {})
|
||||
});
|
||||
|
||||
if (data?.lastSyncTimestamp) {
|
||||
console.log('[SYNC] Sync timestamp change detected', {
|
||||
timestamp: data.lastSyncTimestamp.toDate(),
|
||||
householdId: change.doc.id,
|
||||
queryKey: ["events", user?.uid, isFamilyView]
|
||||
});
|
||||
|
||||
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('[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 useQuery({
|
||||
queryKey: ["events", user?.uid, isFamilyView],
|
||||
queryFn: async () => {
|
||||
console.log(`Fetching events - Family View: ${isFamilyView}, User: ${user?.uid}`);
|
||||
|
||||
const fetchEvents = async (userId: string, familyId: string | undefined, isFamilyView: boolean) => {
|
||||
const db = firestore();
|
||||
const userId = user?.uid;
|
||||
const familyId = profileData?.familyId;
|
||||
let allEvents = [];
|
||||
const eventsQuery = db.collection("Events");
|
||||
let constraints = [];
|
||||
|
||||
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}))
|
||||
constraints = [
|
||||
eventsQuery.where("familyId", "==", familyId).where("private", "==", false),
|
||||
eventsQuery.where("creatorId", "==", userId),
|
||||
eventsQuery.where("attendees", "array-contains", userId)
|
||||
];
|
||||
} 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}))
|
||||
constraints = [
|
||||
eventsQuery.where("creatorId", "==", userId),
|
||||
eventsQuery.where("attendees", "array-contains", userId)
|
||||
];
|
||||
}
|
||||
|
||||
const uniqueEventsMap = new Map();
|
||||
const snapshots = await Promise.all(constraints.map(query => query.get()));
|
||||
|
||||
const uniqueEvents = new Map();
|
||||
const processedHashes = new Set();
|
||||
const creatorIds = new Set();
|
||||
|
||||
allEvents.forEach(event => {
|
||||
const eventHash = createEventHash(event);
|
||||
snapshots.forEach(snapshot => {
|
||||
snapshot.docs.forEach(doc => {
|
||||
const event = doc.data();
|
||||
const hash = 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}`);
|
||||
if (!processedHashes.has(hash)) {
|
||||
processedHashes.add(hash);
|
||||
creatorIds.add(event.creatorId);
|
||||
uniqueEvents.set(doc.id, event);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Processing ${uniqueEventsMap.size} unique events after deduplication`);
|
||||
const creatorIdsArray = Array.from(creatorIds);
|
||||
const creatorProfiles = new Map();
|
||||
|
||||
const processedEvents = await Promise.all(
|
||||
Array.from(uniqueEventsMap.values()).map(async (event) => {
|
||||
const profileSnapshot = await db
|
||||
for (let i = 0; i < creatorIdsArray.length; i += 10) {
|
||||
const chunk = creatorIdsArray.slice(i, i + 10);
|
||||
const profilesSnapshot = await db
|
||||
.collection("Profiles")
|
||||
.doc(event.creatorId)
|
||||
.where(firestore.FieldPath.documentId(), "in", chunk)
|
||||
.get();
|
||||
|
||||
const profileData = profileSnapshot.data();
|
||||
const eventColor = profileData?.eventColor || colorMap.pink;
|
||||
profilesSnapshot.docs.forEach(doc => {
|
||||
creatorProfiles.set(doc.id, doc.data()?.eventColor || colorMap.pink);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
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),
|
||||
@ -214,21 +82,66 @@ export const useGetEvents = () => {
|
||||
? 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,
|
||||
eventColor: creatorProfiles.get(event.creatorId) || colorMap.pink,
|
||||
notes: event.notes
|
||||
}));
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const useGetEvents = () => {
|
||||
const { user, profileData } = useAuthContext();
|
||||
const isFamilyView = useAtomValue(isFamilyViewAtom);
|
||||
const queryClient = useQueryClient();
|
||||
const lastSyncTimestamp = useRef<number>(0);
|
||||
|
||||
console.log(`Events processing completed, returning ${processedEvents.length} events`);
|
||||
return processedEvents;
|
||||
},
|
||||
useEffect(() => {
|
||||
if (!user?.uid || !profileData?.familyId) return;
|
||||
|
||||
const prefetchEvents = async () => {
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: ["events", user.uid, false], // Personal events
|
||||
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,
|
||||
cacheTime: 30 * 60 * 1000,
|
||||
keepPreviousData: true,
|
||||
onError: (error) => {
|
||||
console.error('Error fetching events:', error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
prefetchEvents();
|
||||
|
||||
const unsubscribe = firestore()
|
||||
.collection('Households')
|
||||
.where("familyId", "==", profileData.familyId)
|
||||
.onSnapshot((snapshot) => {
|
||||
snapshot.docChanges().forEach((change) => {
|
||||
if (change.type === 'modified') {
|
||||
const data = change.doc.data();
|
||||
if (data?.lastSyncTimestamp) {
|
||||
const newTimestamp = data.lastSyncTimestamp.seconds;
|
||||
if (newTimestamp > lastSyncTimestamp.current) {
|
||||
lastSyncTimestamp.current = newTimestamp;
|
||||
// Invalidate both queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["events", user.uid]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, console.error);
|
||||
|
||||
return unsubscribe;
|
||||
}, [profileData?.familyId, user?.uid, queryClient]);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["events", user?.uid, isFamilyView],
|
||||
queryFn: () => fetchEvents(user?.uid!, profileData?.familyId, isFamilyView),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
gcTime: Infinity,
|
||||
placeholderData: (previousData) => previousData,
|
||||
enabled: Boolean(user?.uid),
|
||||
});
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import {useQuery} from "react-query";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { useQuery } from "react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
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 {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";
|
||||
|
||||
export const useGetHouseholdName = (familyId: string) => {
|
||||
return useQuery(
|
||||
["getHouseholdName", familyId], // Unique query key
|
||||
async () => {
|
||||
console.log(`Fetching household name for familyId: ${familyId}`);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["household", familyId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
// Query the Households collection for the given familyId
|
||||
const snapshot = await firestore()
|
||||
.collection("Households")
|
||||
.where("familyId", "==", familyId)
|
||||
.get();
|
||||
|
||||
if (!snapshot.empty) {
|
||||
// Extract the name from the first matching document
|
||||
const householdData = snapshot.docs[0].data();
|
||||
console.log("Household found:", householdData);
|
||||
return householdData.name || null; // Return the name or null if missing
|
||||
} else {
|
||||
console.log("No household found for the given familyId.");
|
||||
return null; // Return null if no household found
|
||||
return householdData.name ?? null;
|
||||
}
|
||||
|
||||
console.log("No household found for the given familyId.");
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error("Error fetching household name:", e);
|
||||
throw e; // Ensure error propagates to the query error handling
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: !!familyId, // Only fetch if familyId is provided
|
||||
staleTime: 5 * 60 * 1000, // Cache the data for 5 minutes
|
||||
}
|
||||
);
|
||||
enabled: Boolean(familyId),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { useQuery } from "react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
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 { useAuthContext } from "@/contexts/AuthContext";
|
||||
|
||||
@ -35,11 +35,11 @@ export const useGetNotifications = () => {
|
||||
const snapshot = await firestore()
|
||||
.collection("Notifications")
|
||||
.where("familyId", "==", profileData?.familyId)
|
||||
.orderBy("timestamp", "desc")
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => {
|
||||
const data = doc.data() as NotificationFirestore;
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
...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 { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||
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 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 auth from "@react-native-firebase/auth";
|
||||
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 { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||
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";
|
||||
|
||||
export const useResetPassword = () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useMutation} from "react-query";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||
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";
|
||||
|
||||
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 {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 {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||
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 {EventData} from "@/hooks/firebase/types/eventData";
|
||||
|
||||
@ -18,7 +18,7 @@ export const useUpdateEvent = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClients.invalidateQueries("events")
|
||||
queryClients.invalidateQueries({queryKey: ["events"]})
|
||||
}
|
||||
})
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
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 { IFeedback } from "@/contexts/FeedbackContext";
|
||||
|
||||
@ -54,7 +54,7 @@ export const useUpdateFeedback = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: (updatedFeedback) => {
|
||||
queryClient.invalidateQueries("feedbacks");
|
||||
queryClient.invalidateQueries({queryKey: ["feedbacks"]})
|
||||
|
||||
queryClient.setQueryData(
|
||||
["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 {IGrocery} from "@/hooks/firebase/types/groceryData";
|
||||
|
||||
@ -18,7 +18,7 @@ export const useUpdateGrocery = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClients.invalidateQueries("groceries")
|
||||
queryClients.invalidateQueries({queryKey: ["groceries"]})
|
||||
}
|
||||
})
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const useUpdateHouseholdName = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@ -44,7 +44,7 @@ export const useUpdateHouseholdName = () => {
|
||||
}
|
||||
},
|
||||
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 { useMutation, useQueryClient } from "react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import { IBrainDump } from "@/contexts/DumpContext";
|
||||
|
||||
@ -54,7 +54,7 @@ export const useUpdateNote = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: (updatedNote) => {
|
||||
queryClient.invalidateQueries("braindumps");
|
||||
queryClient.invalidateQueries({queryKey: ["braindumps"]});
|
||||
|
||||
queryClient.setQueryData(
|
||||
["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 {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||
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 {IToDo, REPEAT_TYPE} from "@/hooks/firebase/types/todoData";
|
||||
import {addDays, addMonths, addWeeks, addYears, compareAsc, format, subDays} from "date-fns";
|
||||
@ -165,7 +165,7 @@ export const useUpdateTodo = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClients.invalidateQueries("todos")
|
||||
queryClients.invalidateQueries({queryKey: ["todos"]})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
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 {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
||||
|
||||
@ -51,7 +51,7 @@ export const useUpdateUserData = () => {
|
||||
}
|
||||
},
|
||||
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 AppleAuthentication from "expo-apple-authentication";
|
||||
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";
|
||||
|
||||
const googleConfig = {
|
||||
@ -302,7 +302,7 @@ export const useCalSync = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleNotification = async (notification: Notifications.Notification) => {
|
||||
queryClient.invalidateQueries(["events"]);
|
||||
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||
};
|
||||
|
||||
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 {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
||||
import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils";
|
||||
@ -23,7 +23,6 @@ export const useFetchAndSaveAppleEvents = () => {
|
||||
timeMax
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
const items = response ?? [];
|
||||
await createEventsFromProvider(items);
|
||||
} catch (error) {
|
||||
@ -32,7 +31,7 @@ export const useFetchAndSaveAppleEvents = () => {
|
||||
}
|
||||
},
|
||||
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 functions from "@react-native-firebase/functions";
|
||||
|
||||
@ -30,7 +30,7 @@ export const useFetchAndSaveGoogleEvents = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries(["events"]);
|
||||
queryClient.invalidateQueries({queryKey: ["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 { useSetUserData } from "@/hooks/firebase/useSetUserData";
|
||||
import functions from '@react-native-firebase/functions';
|
||||
@ -130,7 +130,7 @@ export const useFetchAndSaveMicrosoftEvents = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries(["events"]);
|
||||
queryClient.invalidateQueries({queryKey: ["events"]});
|
||||
console.log(`Successfully synced ${data.eventCount} Microsoft events`);
|
||||
},
|
||||
onError: (error) => {
|
||||
|
@ -16,8 +16,8 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Cally.app"
|
||||
BlueprintName = "cally"
|
||||
ReferencedContainer = "container:cally.xcodeproj">
|
||||
BlueprintName = "Cally"
|
||||
ReferencedContainer = "container:Cally.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@ -33,9 +33,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||
BuildableName = "callyTests.xctest"
|
||||
BlueprintName = "callyTests"
|
||||
ReferencedContainer = "container:cally.xcodeproj">
|
||||
BuildableName = "CallyTests.xctest"
|
||||
BlueprintName = "CallyTests"
|
||||
ReferencedContainer = "container:Cally.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
@ -56,8 +56,8 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Cally.app"
|
||||
BlueprintName = "cally"
|
||||
ReferencedContainer = "container:cally.xcodeproj">
|
||||
BlueprintName = "Cally"
|
||||
ReferencedContainer = "container:Cally.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
@ -73,8 +73,8 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Cally.app"
|
||||
BlueprintName = "cally"
|
||||
ReferencedContainer = "container:cally.xcodeproj">
|
||||
BlueprintName = "Cally"
|
||||
ReferencedContainer = "container:Cally.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
@ -2,7 +2,7 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:cally.xcodeproj">
|
||||
location = "group:Cally.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
@ -1,17 +1,18 @@
|
||||
#import "AppDelegate.h"
|
||||
#import <Firebase/Firebase.h>
|
||||
#import <Firebase.h>
|
||||
|
||||
#import <React/RCTBundleURLProvider.h>
|
||||
#import <React/RCTLinkingManager.h>
|
||||
#import "RNAppAuthAuthorizationFlowManager.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
self.moduleName = @"main";
|
||||
// @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";
|
||||
|
||||
// You can add your custom initial props in the dictionary below.
|
||||
// They will be passed down to the ViewController used by React Native.
|
||||
self.initialProps = @{};
|