Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Dejan
2024-12-15 22:44:43 +01:00
125 changed files with 6730 additions and 5154 deletions

85
.idea/jsLinters/jshint.xml generated Normal file
View 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>

View File

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

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, {useEffect} from "react";
import {RefreshControl, ScrollView, View} from "react-native";
import CalendarPage from "@/components/pages/calendar/CalendarPage";
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
@ -6,9 +6,9 @@ import * as Device from "expo-device";
import {DeviceType} from "expo-device";
import {useCalSync} from "@/hooks/useCalSync";
import {colorMap} from "@/constants/colorMap";
import { useSetAtom } from "jotai";
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
import { useAuthContext } from "@/contexts/AuthContext";
import {useSetAtom} from "jotai";
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
export default function Screen() {
const isTablet = Device.deviceType === DeviceType.TABLET;
@ -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>
);
}

View File

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

View File

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

View File

@ -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);
};
@ -50,7 +52,7 @@ export const AddEventDialog = () => {
onPress={() => setShow(true)}
>
<View row centerV centerH>
<PlusIcon />
<PlusIcon/>
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
New
</Text>

View 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;

View File

@ -1,600 +1,28 @@
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) => {
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) {
if (isLoading || isSyncing) {
return (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
@ -603,132 +31,22 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
);
}
return (
<>
{isSyncing && (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
)}
<Calendar
bodyContainerStyle={styles.calHeader}
swipeEnabled
mode={mode}
sortedMonthView
events={filteredEvents}
// renderEvent={renderEvent}
eventCellStyle={memoizedEventCellStyle}
allDayEventCellStyle={memoizedEventCellStyle}
// enableEnrichedEvents={true}
// enrichedEventsByDate={enrichedEvents}
onPressEvent={handlePressEvent}
weekStartsOn={memoizedWeekStartsOn}
height={calendarHeight}
activeDate={todaysDate}
date={selectedDate}
onPressCell={handlePressCell}
headerContentStyle={memoizedHeaderContentStyle}
onSwipeEnd={handleSwipeEnd}
scrollOffsetMinutes={offsetMinutes}
theme={{
palette: {
nowIndicator: profileData?.eventColor || "#fd1575",
gray: {
"100": "#e8eaed",
"200": "#e8eaed",
"500": "#b7b7b7",
"800": "#919191",
},
},
typography: {
fontFamily: "PlusJakartaSans_500Medium",
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
xl: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 14,
},
moreLabel: {},
xs: {fontSize: 10},
},
}}
dayHeaderStyle={dateStyle}
dayHeaderHighlightColor={"white"}
showAdjacentMonths
headerContainerStyle={mode !== "month" ? {
overflow: "hidden",
} : {}}
hourStyle={styles.hourStyle}
onPressDateHeader={handlePressDayHeader}
ampm
// renderCustomDateForMonth={renderCustomDateForMonth}
/>
<View style={{backgroundColor: 'white', height: 50, width: '100%'}}/>
</>
);
}
);
return mode === "month"
? <MonthCalendar {...props} />
: <DetailedCalendar {...props} />;
});
const styles = StyleSheet.create({
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;

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

View File

@ -2,7 +2,7 @@ import {
Button,
ButtonSize,
Colors,
DateTimePicker,
DateTimePicker, Dialog,
LoaderScreen,
Modal,
Picker,
@ -14,47 +14,48 @@ import {
TouchableOpacity,
View,
} from "react-native-ui-lib";
import { ScrollView } from "react-native-gesture-handler";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useEffect, useRef, useState } from "react";
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
import { PickerMultiValue } from "react-native-ui-lib/src/components/picker/types";
import { useCreateEvent } from "@/hooks/firebase/useCreateEvent";
import { EventData } from "@/hooks/firebase/types/eventData";
import {ScrollView} from "react-native-gesture-handler";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import {useEffect, useRef, useState} from "react";
import {AntDesign, Feather, Ionicons} from "@expo/vector-icons";
import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateEvent} from "@/hooks/firebase/useCreateEvent";
import {EventData} from "@/hooks/firebase/types/eventData";
import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { Alert, StyleSheet } from "react-native";
import {Alert, StyleSheet} from "react-native";
import ClockIcon from "@/assets/svgs/ClockIcon";
import LockIcon from "@/assets/svgs/LockIcon";
import MenuIcon from "@/assets/svgs/MenuIcon";
import CameraIcon from "@/assets/svgs/CameraIcon";
import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
import { useAtom } from "jotai";
import {useAtom} from "jotai";
import {
eventForEditAtom,
selectedNewEventDateAtom,
isAllDayAtom,
} from "@/components/pages/calendar/atoms";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import BinIcon from "@/assets/svgs/BinIcon";
import DeleteEventDialog from "./DeleteEventDialog";
import { useDeleteEvent } from "@/hooks/firebase/useDeleteEvent";
import {useDeleteEvent} from "@/hooks/firebase/useDeleteEvent";
import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
import { addHours, startOfHour, startOfMinute } from "date-fns";
import { useAuthContext } from "@/contexts/AuthContext";
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" },
{ label: "Tuesday", value: "tuesday" },
{ label: "Wednesday", value: "wednesday" },
{ label: "Thursday", value: "thursday" },
{ label: "Friday", value: "friday" },
{ label: "Saturday", value: "saturday" },
{ label: "Sunday", value: "sunday" },
{label: "Monday", value: "monday"},
{label: "Tuesday", value: "tuesday"},
{label: "Wednesday", value: "wednesday"},
{label: "Thursday", value: "thursday"},
{label: "Friday", value: "friday"},
{label: "Saturday", value: "saturday"},
{label: "Sunday", value: "sunday"},
];
export const ManuallyAddEventModal = () => {
const insets = useSafeAreaInsets();
const { user } = useAuthContext();
const {user} = useAuthContext();
const [selectedNewEventDate, setSelectedNewEndDate] = useAtom(
selectedNewEventDateAtom
@ -62,9 +63,9 @@ export const ManuallyAddEventModal = () => {
const [editEvent, setEditEvent] = useAtom(eventForEditAtom);
const [allDayAtom, setAllDayAtom] = useAtom(isAllDayAtom);
const [deleteModalVisible, setDeleteModalVisible] = useState<boolean>(false);
const { mutateAsync: deleteEvent, isLoading: isDeleting } = useDeleteEvent();
const {mutateAsync: deleteEvent, isLoading: isDeleting} = useDeleteEvent();
const { show, close, initialDate } = {
const {show, close, initialDate} = {
show: !!selectedNewEventDate || !!editEvent,
close: () => {
setDeleteModalVisible(false);
@ -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(
@ -126,7 +129,7 @@ export const ManuallyAddEventModal = () => {
isLoading: isAdding,
isError,
} = useCreateEvent();
const { data: members } = useGetFamilyMembers(true);
const {data: members} = useGetFamilyMembers(true);
const titleRef = useRef<TextFieldRef>(null);
const [creator, setCreator] = 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());
@ -193,7 +196,7 @@ export const ManuallyAddEventModal = () => {
};
const handleDeleteEvent = async () => {
await deleteEvent({ eventId: `${editEvent?.id}` });
await deleteEvent({eventId: `${editEvent?.id}`});
close();
};
@ -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
@ -394,14 +561,14 @@ export const ManuallyAddEventModal = () => {
<Button
style={styles.topBtn}
marginL-5
iconSource={() => <BinIcon />}
iconSource={() => <BinIcon/>}
onPress={showDeleteEventModal}
/>
)}
</View>
</View>
{/*)}*/}
<ScrollView style={{ minHeight: "85%" }}>
<ScrollView style={{minHeight: "85%"}}>
<TextField
placeholder="Add event title"
ref={titleRef}
@ -410,161 +577,38 @@ export const ManuallyAddEventModal = () => {
setTitle(text);
}}
placeholderTextColor="#2d2d30"
style={{ fontFamily: "Manrope_500Medium", fontSize: 22 }}
style={{fontFamily: "Manrope_500Medium", fontSize: 22}}
paddingT-15
paddingL-30
returnKeyType="next"
/>
<View style={styles.divider} marginT-8 />
<View marginL-30 centerV>
<View row spread marginB-10 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191" />
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
All day
</Text>
</View>
<View right marginR-30>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isAllDay}
onValueChange={(value) => setIsAllDay(value)}
/>
</View>
</View>
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={startDate}
onChange={(date) => {
setStartDate(date);
<View style={styles.divider} marginT-8/>
{renderDateSection()}
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>
<View style={styles.divider} />
<View style={styles.divider}/>
<View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191" />
<Ionicons name="person-circle-outline" size={28} color="#919191"/>
<Text
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 16 }}
style={{fontFamily: "Manrope_600SemiBold", fontSize: 16}}
marginL-10
>
Attendees
</Text>
<View flex-1 />
<View flex-1/>
<Picker
value={selectedAttendees}
onChange={(value) =>
setSelectedAttendees((value as string[]) ?? [])
}
style={{ marginLeft: "auto" }}
style={{marginLeft: "auto"}}
mode={PickerModes.MULTI}
renderInput={() => (
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c" />
<Ionicons name="add-outline" size={20} color="#ea156c"/>
)}
style={{
marginLeft: "auto",
@ -599,10 +643,10 @@ export const ManuallyAddEventModal = () => {
/>
</View>
<View style={styles.divider} />
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon />
<ClockIcon/>
<Text
style={{
fontFamily: "Manrope_600SemiBold",
@ -618,7 +662,7 @@ export const ManuallyAddEventModal = () => {
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c" />
<Ionicons name="add-outline" size={20} color="#ea156c"/>
)}
style={{
marginLeft: "auto",
@ -627,16 +671,16 @@ export const ManuallyAddEventModal = () => {
borderColor: "#ea156c",
borderWidth: 1,
}}
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
color="#ea156c"
label="Set Reminder"
/>
</View>
</View>
<View style={styles.divider} />
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row center>
<LockIcon />
<LockIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
@ -658,10 +702,10 @@ export const ManuallyAddEventModal = () => {
/>
</View>
</View>
<View style={styles.divider} />
<View style={styles.divider}/>
<View marginH-28 marginB-0 centerV flex-1>
<View row centerV style={{ flexGrow: 1 }}>
<Ionicons name="location-outline" size={25} color={"#919191"} />
<View row centerV style={{flexGrow: 1}}>
<Ionicons name="location-outline" size={25} color={"#919191"}/>
<TextField
placeholder="Location"
value={location}
@ -681,10 +725,10 @@ export const ManuallyAddEventModal = () => {
</View>
{editEvent && (
<>
<View style={styles.divider} />
<View style={styles.divider}/>
<View marginH-32 marginB-0 centerV flex-1>
<View row centerV style={{ flexGrow: 1 }}>
<AddPersonIcon />
<View row centerV style={{flexGrow: 1}}>
<AddPersonIcon/>
<TextField
editable={false}
value={creator}
@ -702,11 +746,11 @@ export const ManuallyAddEventModal = () => {
</View>
</>
)}
<View style={styles.divider} />
<View style={styles.divider}/>
<View marginH-30 marginB-0 spread centerV flex-1>
<TouchableOpacity onPress={() => detailsRef?.current?.focus()}>
<View row centerV>
<MenuIcon />
<MenuIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
@ -727,7 +771,7 @@ export const ManuallyAddEventModal = () => {
multiline
numberOfLines={10}
marginT-10
style={{ flex: 1, minHeight: 180 }}
style={{flex: 1, minHeight: 180}}
/>
</View>
</ScrollView>
@ -737,12 +781,12 @@ export const ManuallyAddEventModal = () => {
marginB-30
label="Create event from image"
text70
style={{ height: 47 }}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 15 }}
style={{height: 47}}
labelStyle={{fontFamily: "PlusJakartaSans_500Medium", fontSize: 15}}
backgroundColor="#05a8b6"
iconSource={() => (
<View marginR-5>
<CameraIcon color="white" />
<CameraIcon color="white"/>
</View>
)}
/>
@ -756,11 +800,12 @@ export const ManuallyAddEventModal = () => {
/>
)}
</Modal>
</>
);
};
const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 },
divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
gradient: {
height: "25%",
position: "absolute",
@ -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,
}
});

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

View File

@ -1 +0,0 @@

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

View 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
});
};

View File

@ -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 (

View File

@ -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,13 +305,29 @@ const MyProfile = () => {
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)} disabled={takenColors.includes(colorMap.green)}>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightOrange)} disabled={takenColors.includes(colorMap.lightOrange)}>
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightOrange) ? 0.1 : 1}]} backgroundColor={colorMap.lightOrange}>
{selectedColor == colorMap.lightOrange && (
<AntDesign name="check" size={30} color="black" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.green)}disabled={takenColors.includes(colorMap.green)}>
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.green) ? 0.1 : 1}]} backgroundColor={colorMap.green}>
{selectedColor == colorMap.green && (
<AntDesign name="check" size={30} color="white" />
)}
</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,47 +335,24 @@ const MyProfile = () => {
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.purple)}disabled={takenColors.includes(colorMap.purple)}>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.lightTeal)} disabled={takenColors.includes(colorMap.lightTeal)}>
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.lightTeal) ? 0.1 : 1}]} backgroundColor={colorMap.lightTeal}>
{selectedColor == colorMap.lightTeal && (
<AntDesign name="check" size={30} color="black" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleChangeColor(colorMap.purple)} disabled={takenColors.includes(colorMap.purple)}>
<View style={[styles.colorBox, {opacity: takenColors.includes(colorMap.purple) ? 0.1 : 1}]} backgroundColor={colorMap.purple}>
{selectedColor == colorMap.purple && (
<AntDesign name="check" size={30} color="white" />
)}
</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>

View File

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

View File

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

View File

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

View 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>
);
}

View File

@ -0,0 +1,3 @@
{
"expo": {}
}

View File

@ -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}`,

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -1,16 +1,15 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import { useAuthContext } from "@/contexts/AuthContext";
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")
@ -25,23 +24,21 @@ export const useCreateFeedback = () => {
.set({
...feedback,
creatorId: currentUser?.uid,
}, {merge: true});
}, { merge: true });
return;
}
}
const newDoc = firestore().collection('Feedbacks').doc();
await firestore()
.collection("Feedbacks")
.add({...feedback, id: newDoc.id, creatorId: currentUser?.uid});
} catch (e) {
console.error(e);
}
.add({ ...feedback, id: newDoc.id, creatorId: currentUser?.uid });
},
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"] });
}
});
};

View File

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

View File

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

View File

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

View File

@ -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"]})
}
})
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import {useMutation, useQueryClient} from "react-query";
import {useAuthContext} from "@/contexts/AuthContext";
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";
import { Notification } from "@/hooks/firebase/useGetNotifications";
export const useDeleteNotification = () => {
const queryClient = useQueryClient();
const {user} = useAuthContext();
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};
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 });
}
});
};

View File

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

View File

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

View File

@ -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 = () => {

View File

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

View File

@ -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,
cacheTime: 30 * 60 * 1000,
keepPreviousData: true,
onError: (error) => {
console.error('Error fetching events:', error);
});
await queryClient.prefetchQuery({
queryKey: ["events", user.uid, true], // Family events
queryFn: () => fetchEvents(user.uid, profileData.familyId, true),
staleTime: 5 * 60 * 1000,
});
};
prefetchEvents();
const unsubscribe = firestore()
.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),
});
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = () => {

View File

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

View File

@ -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 = () => {

View File

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

View File

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

View File

@ -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"]})
}
})
}

View File

@ -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],

View File

@ -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"]})
}
})
}

View File

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

View File

@ -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],

View File

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

View File

@ -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"]})
}
})
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

File diff suppressed because one or more lines are too long

View File

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

View File

@ -2,7 +2,7 @@
<Workspace
version = "1.0">
<FileRef
location = "group:cally.xcodeproj">
location = "group:Cally.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">

View File

@ -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
{
// @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";
[FIRApp configure];
// 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 = @{};

Some files were not shown because too many files have changed in this diff Show More