mirror of
https://github.com/urosran/cally.git
synced 2025-11-26 00:24:53 +00:00
Calendar improvements
This commit is contained in:
@ -20,7 +20,7 @@ const UsersList = () => {
|
||||
const { user: currentUser } = useAuthContext();
|
||||
const { data: familyMembers, refetch: refetchFamilyMembers } =
|
||||
useGetFamilyMembers();
|
||||
const [selectedUser, setSelectedUser] = useAtom<UserProfile | null>(selectedUserAtom);
|
||||
const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
|
||||
|
||||
useEffect(() => {
|
||||
refetchFamilyMembers();
|
||||
|
||||
27
components/pages/calendar/CalendarController.tsx
Normal file
27
components/pages/calendar/CalendarController.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import {useAtomValue} from 'jotai';
|
||||
import {selectedDateAtom} from '@/components/pages/calendar/atoms';
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
import {useDidUpdate} from "react-native-ui-lib/src/hooks";
|
||||
|
||||
interface CalendarControllerProps {
|
||||
scrollViewRef: React.RefObject<FlashList<any>>;
|
||||
centerMonthIndex: number;
|
||||
}
|
||||
|
||||
export const CalendarController: React.FC<CalendarControllerProps> = (
|
||||
{
|
||||
scrollViewRef,
|
||||
centerMonthIndex
|
||||
}) => {
|
||||
const selectedDate = useAtomValue(selectedDateAtom);
|
||||
|
||||
useDidUpdate(() => {
|
||||
scrollViewRef.current?.scrollToIndex({
|
||||
index: centerMonthIndex,
|
||||
animated: false
|
||||
})
|
||||
}, [selectedDate, centerMonthIndex]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,121 +1,108 @@
|
||||
import React, {memo, useState} from "react";
|
||||
import React, {memo, useState, useMemo, useCallback} from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
|
||||
import {MaterialIcons} from "@expo/vector-icons";
|
||||
import {months} from "./constants";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {useAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {format, isSameDay} from "date-fns";
|
||||
import {format} from "date-fns";
|
||||
import * as Device from "expo-device";
|
||||
import {Mode} from "react-native-big-calendar";
|
||||
import {useIsFetching} from "@tanstack/react-query";
|
||||
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {months} from "./constants";
|
||||
import RefreshButton from "@/components/shared/RefreshButton";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
|
||||
type ViewMode = "day" | "week" | "month" | "3days";
|
||||
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
|
||||
const SEGMENTS = isTablet
|
||||
? [{label: "D"}, {label: "W"}, {label: "M"}]
|
||||
: [{label: "D"}, {label: "3D"}, {label: "M"}];
|
||||
|
||||
const MODE_MAP = {
|
||||
tablet: ["day", "week", "month"],
|
||||
mobile: ["day", "3days", "month"]
|
||||
} as const;
|
||||
|
||||
export const CalendarHeader = memo(() => {
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const [tempIndex, setTempIndex] = useState<number | null>(null);
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
|
||||
const segments = isTablet
|
||||
? [{label: "D"}, {label: "W"}, {label: "M"}]
|
||||
: [{label: "D"}, {label: "3D"}, {label: "M"}];
|
||||
const {resyncAllCalendars, isSyncing} = useCalSync();
|
||||
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
|
||||
|
||||
const handleSegmentChange = (index: number) => {
|
||||
let selectedMode: Mode;
|
||||
if (isTablet) {
|
||||
selectedMode = ["day", "week", "month"][index] as Mode;
|
||||
} else {
|
||||
selectedMode = ["day", "3days", "month"][index] as Mode;
|
||||
}
|
||||
const isLoading = useMemo(() => isSyncing || isFetching, [isSyncing, isFetching]);
|
||||
|
||||
const handleSegmentChange = useCallback((index: number) => {
|
||||
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
|
||||
const selectedMode = modes[index] as ViewMode;
|
||||
|
||||
setTempIndex(index);
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode);
|
||||
setTempIndex(null);
|
||||
}, 150);
|
||||
}, [setMode]);
|
||||
|
||||
if (selectedMode) {
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode as "day" | "week" | "month" | "3days");
|
||||
setTempIndex(null);
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonthChange = (month: string) => {
|
||||
const currentDay = selectedDate.getDate();
|
||||
const currentYear = selectedDate.getFullYear();
|
||||
const handleMonthChange = useCallback((month: string) => {
|
||||
const newMonthIndex = months.indexOf(month);
|
||||
|
||||
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
|
||||
const updatedDate = new Date(
|
||||
selectedDate.getFullYear(),
|
||||
newMonthIndex,
|
||||
selectedDate.getDate()
|
||||
);
|
||||
setSelectedDate(updatedDate);
|
||||
};
|
||||
}, [selectedDate, setSelectedDate]);
|
||||
|
||||
const isSelectedDateToday = isSameDay(selectedDate, new Date());
|
||||
|
||||
const getInitialIndex = () => {
|
||||
if (isTablet) {
|
||||
switch (mode) {
|
||||
case "day":
|
||||
return 0;
|
||||
case "week":
|
||||
return 1;
|
||||
case "month":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
switch (mode) {
|
||||
case "day":
|
||||
return 0;
|
||||
case "3days":
|
||||
return 1;
|
||||
case "month":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
const handleRefresh = useCallback(async () => {
|
||||
try {
|
||||
await resyncAllCalendars();
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
}
|
||||
};
|
||||
}, [resyncAllCalendars]);
|
||||
|
||||
const getInitialIndex = useCallback(() => {
|
||||
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
|
||||
//@ts-ignore
|
||||
return modes.indexOf(mode);
|
||||
}, [mode]);
|
||||
|
||||
const renderMonthPicker = () => (
|
||||
<View row centerV gap-1 flexS>
|
||||
{isTablet && (
|
||||
<Text style={styles.yearText}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
)}
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder="Select Month"
|
||||
style={styles.monthPicker}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={value => handleMonthChange(value as string)}
|
||||
trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: styles.yearText,
|
||||
}}
|
||||
>
|
||||
{months.map(month => (
|
||||
<Picker.Item key={month} label={month} value={month}/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: isTablet ? 8 : 0,
|
||||
borderRadius: 20,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
centerV
|
||||
>
|
||||
<View row centerV gap-3>
|
||||
{isTablet && (
|
||||
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
)}
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder={"Select Month"}
|
||||
style={{fontFamily: "Manrope_500Medium", fontSize: 17, width: 85}}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={(itemValue) => handleMonthChange(itemValue as string)}
|
||||
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"}/>}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17},
|
||||
}}
|
||||
>
|
||||
{months.map((month) => (
|
||||
<Picker.Item key={month} label={month} value={month}/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
<View style={styles.container} flexG centerV>
|
||||
{mode !== "month" ? renderMonthPicker() : <View flexG/>}
|
||||
|
||||
<View row centerV>
|
||||
<View row centerV flexS>
|
||||
<Button
|
||||
size={"xSmall"}
|
||||
size="xSmall"
|
||||
marginR-1
|
||||
avoidInnerPadding
|
||||
style={styles.todayButton}
|
||||
@ -124,27 +111,52 @@ export const CalendarHeader = memo(() => {
|
||||
<MaterialIcons name="calendar-today" size={30} color="#5f6368"/>
|
||||
<Text style={styles.todayDate}>{format(new Date(), "d")}</Text>
|
||||
</Button>
|
||||
<View>
|
||||
|
||||
<View style={styles.segmentContainer}>
|
||||
<SegmentedControl
|
||||
segments={segments}
|
||||
segments={SEGMENTS}
|
||||
backgroundColor="#ececec"
|
||||
inactiveColor="#919191"
|
||||
activeBackgroundColor="#ea156c"
|
||||
activeColor="white"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
segmentLabelStyle={styles.segmentslblStyle}
|
||||
segmentLabelStyle={styles.segmentLabel}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={tempIndex ?? getInitialIndex()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<RefreshButton onRefresh={handleRefresh} isSyncing={isLoading}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
segmentslblStyle: {
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: isTablet ? 8 : 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
paddingLeft: 10
|
||||
},
|
||||
yearText: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 17,
|
||||
},
|
||||
monthPicker: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 17,
|
||||
width: 85,
|
||||
},
|
||||
segmentContainer: {
|
||||
maxWidth: 120,
|
||||
height: 40,
|
||||
},
|
||||
segmentLabel: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
|
||||
@ -9,11 +9,6 @@ export default function CalendarPage() {
|
||||
paddingH-0
|
||||
paddingT-0
|
||||
>
|
||||
{/*<HeaderTemplate
|
||||
message={"Let's get your week started !"}
|
||||
isWelcome
|
||||
isCalendar={true}
|
||||
/>*/}
|
||||
<InnerCalendar />
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} 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 React, {useCallback, useMemo, useRef, useState} from "react";
|
||||
import {View} from "react-native-ui-lib";
|
||||
import {DeviceType} from "expo-device";
|
||||
import * as Device from "expo-device";
|
||||
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||
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";
|
||||
import {View} from "react-native-ui-lib";
|
||||
import {DeviceType} from "expo-device";
|
||||
import * as Device from "expo-device"
|
||||
import {useAtomCallback} from 'jotai/utils'
|
||||
import {DetailedCalendarController} from "@/components/pages/calendar/DetailedCalendarController";
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarHeight: number;
|
||||
calendarWidth: number;
|
||||
mode: "week" | "month" | "day" | "3days";
|
||||
onLoad?: () => void;
|
||||
@ -36,14 +35,14 @@ const MODE_TO_DAYS = {
|
||||
'3days': 3,
|
||||
'day': 1,
|
||||
'month': 1
|
||||
};
|
||||
} as const;
|
||||
|
||||
const getContainerProps = (selectedDate: Date) => ({
|
||||
const getContainerProps = (date: Date, customKey: string) => ({
|
||||
hourWidth: 70,
|
||||
allowPinchToZoom: true,
|
||||
useHaptic: true,
|
||||
scrollToNow: true,
|
||||
initialDate: selectedDate.toISOString(),
|
||||
initialDate: customKey !== "default" ? customKey : date.toISOString(),
|
||||
});
|
||||
|
||||
const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
|
||||
@ -51,37 +50,23 @@ const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
|
||||
prev.event.lastModified === next.event.lastModified;
|
||||
});
|
||||
|
||||
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
||||
{
|
||||
calendarHeight,
|
||||
calendarWidth,
|
||||
mode,
|
||||
onLoad
|
||||
}) => {
|
||||
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(({
|
||||
calendarWidth,
|
||||
mode,
|
||||
onLoad
|
||||
}) => {
|
||||
const {profileData} = useAuthContext();
|
||||
const selectedDate = useAtomValue(selectedDateAtom);
|
||||
const {data: familyMembers} = useGetFamilyMembers();
|
||||
const calendarRef = useRef<CalendarKitHandle>(null);
|
||||
const {data: events} = useGetEvents();
|
||||
const selectedUser = useAtomValue(selectedUserAtom);
|
||||
const calendarRef = useRef<CalendarKitHandle>(null);
|
||||
const [customKey, setCustomKey] = useState("defaultKey");
|
||||
|
||||
const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]);
|
||||
const containerProps = useMemo(() => getContainerProps(selectedDate), [selectedDate]);
|
||||
const currentDate = useMemo(() => new Date(), []);
|
||||
const containerProps = useMemo(() => getContainerProps(currentDate, customKey), [currentDate, customKey]);
|
||||
|
||||
const checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
|
||||
const currentMode = get(modeAtom);
|
||||
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
|
||||
calendarRef?.current?.goToDate({date: selectedDate});
|
||||
setCustomKey(selectedDate.toISOString())
|
||||
}
|
||||
}, [selectedDate]));
|
||||
|
||||
useEffect(() => {
|
||||
checkModeAndGoToDate();
|
||||
}, [selectedDate, checkModeAndGoToDate]);
|
||||
|
||||
const {data: formattedEvents} = useFormattedEvents(events ?? [], selectedDate, selectedUser);
|
||||
const {data: formattedEvents} = useFormattedEvents(events ?? [], currentDate, selectedUser);
|
||||
const {
|
||||
handlePressEvent,
|
||||
handlePressCell,
|
||||
@ -115,9 +100,12 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
||||
onPressEvent={handlePressEvent}
|
||||
onPressBackground={handlePressCell}
|
||||
onLoad={onLoad}
|
||||
key={customKey}
|
||||
>
|
||||
<CalendarHeader {...HEADER_PROPS} />
|
||||
<DetailedCalendarController
|
||||
calendarRef={calendarRef}
|
||||
setCustomKey={setCustomKey}
|
||||
/>
|
||||
<CalendarHeader {...HEADER_PROPS}/>
|
||||
<CalendarBody
|
||||
{...BODY_PROPS}
|
||||
renderEvent={renderEvent}
|
||||
|
||||
38
components/pages/calendar/DetailedCalendarController.tsx
Normal file
38
components/pages/calendar/DetailedCalendarController.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, {useCallback, useEffect, useRef} from 'react';
|
||||
import {useAtomValue} from 'jotai';
|
||||
import {useAtomCallback} from 'jotai/utils';
|
||||
import {modeAtom, selectedDateAtom} from '@/components/pages/calendar/atoms';
|
||||
import {isToday} from 'date-fns';
|
||||
import {CalendarKitHandle} from "@howljs/calendar-kit";
|
||||
|
||||
interface DetailedCalendarDateControllerProps {
|
||||
calendarRef: React.RefObject<CalendarKitHandle>;
|
||||
setCustomKey: (key: string) => void;
|
||||
}
|
||||
|
||||
export const DetailedCalendarController: React.FC<DetailedCalendarDateControllerProps> = ({
|
||||
calendarRef,
|
||||
setCustomKey
|
||||
}) => {
|
||||
const selectedDate = useAtomValue(selectedDateAtom);
|
||||
const lastSelectedDate = useRef(selectedDate);
|
||||
|
||||
const checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
|
||||
const currentMode = get(modeAtom);
|
||||
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
|
||||
if (currentMode === "month") {
|
||||
setCustomKey(selectedDate.toISOString());
|
||||
}
|
||||
calendarRef?.current?.goToDate({date: selectedDate});
|
||||
}
|
||||
}, [selectedDate, calendarRef, setCustomKey]));
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate !== lastSelectedDate.current) {
|
||||
checkModeAndGoToDate();
|
||||
lastSelectedDate.current = selectedDate;
|
||||
}
|
||||
}, [selectedDate, checkModeAndGoToDate]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -1,33 +1,31 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, ActivityIndicator } from 'react-native';
|
||||
import { Text } from 'react-native-ui-lib';
|
||||
import {StyleSheet, View, ActivityIndicator} from 'react-native';
|
||||
import {Text} from 'react-native-ui-lib';
|
||||
import Animated, {
|
||||
withTiming,
|
||||
useAnimatedStyle,
|
||||
FadeOut,
|
||||
useSharedValue,
|
||||
runOnJS
|
||||
} from 'react-native-reanimated';
|
||||
import { useGetEvents } from '@/hooks/firebase/useGetEvents';
|
||||
import { useCalSync } from '@/hooks/useCalSync';
|
||||
import { useSyncEvents } from '@/hooks/useSyncOnScroll';
|
||||
import { useAtom } from 'jotai';
|
||||
import { modeAtom } from './atoms';
|
||||
import { MonthCalendar } from "@/components/pages/calendar/MonthCalendar";
|
||||
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
|
||||
import {useCalSync} from '@/hooks/useCalSync';
|
||||
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
|
||||
import {useAtom} from 'jotai';
|
||||
import {modeAtom} from './atoms';
|
||||
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
|
||||
import DetailedCalendar from "@/components/pages/calendar/DetailedCalendar";
|
||||
import * as Device from "expo-device";
|
||||
|
||||
export type CalendarMode = 'month' | 'day' | '3days' | 'week';
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarHeight: number;
|
||||
calendarWidth: number;
|
||||
}
|
||||
|
||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
|
||||
const { isLoading } = useGetEvents();
|
||||
const {isLoading} = useGetEvents();
|
||||
const [mode] = useAtom<CalendarMode>(modeAtom);
|
||||
const { isSyncing } = useSyncEvents();
|
||||
const {isSyncing} = useSyncEvents();
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
const isCalendarReady = useSharedValue(false);
|
||||
useCalSync();
|
||||
@ -37,26 +35,26 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
|
||||
}, []);
|
||||
|
||||
const containerStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(isCalendarReady.value ? 1 : 0, { duration: 500 }),
|
||||
opacity: withTiming(isCalendarReady.value ? 1 : 0, {duration: 500}),
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const monthStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === 'month' ? 1 : 0, { duration: 300 }),
|
||||
opacity: withTiming(mode === 'month' ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const detailedDayStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === 'day' ? 1 : 0, { duration: 300 }),
|
||||
opacity: withTiming(mode === 'day' ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const detailedMultiStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, { duration: 300 }),
|
||||
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@ -64,7 +62,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
{(isLoading || isSyncing) && mode !== 'month' && (
|
||||
{(isLoading || isSyncing) && mode !== 'month' && (
|
||||
<Animated.View
|
||||
exiting={FadeOut.duration(300)}
|
||||
style={styles.loadingContainer}
|
||||
@ -75,14 +73,19 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
|
||||
)}
|
||||
<Animated.View style={containerStyle}>
|
||||
<Animated.View style={monthStyle} pointerEvents={mode === 'month' ? 'auto' : 'none'}>
|
||||
<MonthCalendar {...props} />
|
||||
<MonthCalendar/>
|
||||
</Animated.View>
|
||||
<Animated.View style={detailedDayStyle} pointerEvents={mode === 'day' ? 'auto' : 'none'}>
|
||||
<DetailedCalendar mode="day" {...props} />
|
||||
</Animated.View>
|
||||
<Animated.View style={detailedMultiStyle} pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
|
||||
<Animated.View style={detailedMultiStyle}
|
||||
pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
|
||||
{!isLoading && (
|
||||
<DetailedCalendar onLoad={handleRenderComplete} mode={isTablet ? 'week' : '3days'} {...props} />
|
||||
<DetailedCalendar
|
||||
onLoad={handleRenderComplete}
|
||||
mode={isTablet ? 'week' : '3days'}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
||||
@ -4,19 +4,16 @@ import {LayoutChangeEvent} from "react-native";
|
||||
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
|
||||
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
|
||||
import {ManuallyAddEventModal} from "@/components/pages/calendar/ManuallyAddEventModal";
|
||||
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
|
||||
import {EventCalendar} from "@/components/pages/calendar/EventCalendar";
|
||||
|
||||
export const InnerCalendar = () => {
|
||||
const [calendarHeight, setCalendarHeight] = useState(0);
|
||||
const [calendarWidth, setCalendarWidth] = useState(0);
|
||||
const calendarContainerRef = useRef(null);
|
||||
const hasSetInitialSize = useRef(false);
|
||||
|
||||
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
if (!hasSetInitialSize.current) {
|
||||
const {height, width} = event.nativeEvent.layout;
|
||||
setCalendarHeight(height);
|
||||
const width = event.nativeEvent.layout.width;
|
||||
setCalendarWidth(width);
|
||||
hasSetInitialSize.current = true;
|
||||
}
|
||||
@ -30,12 +27,9 @@ export const InnerCalendar = () => {
|
||||
onLayout={onLayout}
|
||||
paddingB-0
|
||||
>
|
||||
{calendarHeight > 0 && (
|
||||
<EventCalendar
|
||||
calendarHeight={calendarHeight}
|
||||
calendarWidth={calendarWidth}
|
||||
/>
|
||||
)}
|
||||
<EventCalendar
|
||||
calendarWidth={calendarWidth}
|
||||
/>
|
||||
</View>
|
||||
<CalendarViewSwitch/>
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ export const ManuallyAddEventModal = () => {
|
||||
|
||||
const {
|
||||
mutateAsync: createEvent,
|
||||
isLoading: isAdding,
|
||||
isPending: isAdding,
|
||||
isError,
|
||||
} = useCreateEvent();
|
||||
const {data: members} = useGetFamilyMembers(true);
|
||||
@ -178,15 +178,6 @@ export const ManuallyAddEventModal = () => {
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
const formatDateTime = (date?: Date | string) => {
|
||||
if (!date) return undefined;
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const showDeleteEventModal = () => {
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
@ -257,38 +248,6 @@ export const ManuallyAddEventModal = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const getRepeatLabel = () => {
|
||||
const selectedDays = repeatInterval;
|
||||
const allDays = [
|
||||
"sunday",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
];
|
||||
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
|
||||
|
||||
const isEveryWorkDay = workDays.every((day) => selectedDays.includes(day));
|
||||
|
||||
const isEveryDay = allDays.every((day) => selectedDays.includes(day));
|
||||
|
||||
if (isEveryDay) {
|
||||
return "Every day";
|
||||
} else if (
|
||||
isEveryWorkDay &&
|
||||
!selectedDays.includes("saturday") &&
|
||||
!selectedDays.includes("sunday")
|
||||
) {
|
||||
return "Every work day";
|
||||
} else {
|
||||
return selectedDays
|
||||
.map((item) => daysOfWeek.find((day) => day.value === item)?.label)
|
||||
.join(", ");
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && !isError) {
|
||||
return (
|
||||
<Modal
|
||||
@ -355,7 +314,7 @@ export const ManuallyAddEventModal = () => {
|
||||
setDate(newDate);
|
||||
|
||||
if(isStart) {
|
||||
if (startTime.getHours() > endTime.getHours() &&
|
||||
if (startTime.getHours() > endTime.getHours() &&
|
||||
(isSameDay(newDate, endDate) || isAfter(newDate, endDate))) {
|
||||
const newEndDate = new Date(newDate);
|
||||
newEndDate.setDate(newEndDate.getDate() + 1);
|
||||
@ -364,7 +323,7 @@ export const ManuallyAddEventModal = () => {
|
||||
setEndDate(newEndDate);
|
||||
}
|
||||
} else {
|
||||
if (endTime.getHours() < startTime.getHours() &&
|
||||
if (endTime.getHours() < startTime.getHours() &&
|
||||
(isSameDay(newDate, startDate) || isAfter(startDate, newDate))) {
|
||||
const newStartDate = new Date(newDate);
|
||||
newStartDate.setDate(newStartDate.getDate() - 1);
|
||||
@ -432,7 +391,7 @@ export const ManuallyAddEventModal = () => {
|
||||
is24Hour={profileData?.userType === ProfileType.PARENT ? false : true}
|
||||
onChange={(time) => {
|
||||
if (endDate.getDate() === startDate.getDate() &&
|
||||
time.getHours() >= endTime.getHours() && time.getMinutes() >= endTime.getHours())
|
||||
time.getHours() >= endTime.getHours() && time.getMinutes() >= endTime.getHours())
|
||||
{
|
||||
const newEndDate = new Date(endDate);
|
||||
|
||||
@ -801,9 +760,9 @@ export const ManuallyAddEventModal = () => {
|
||||
<CameraIcon color="white"/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
/>
|
||||
{editEvent && (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
onPress={showDeleteEventModal}
|
||||
style={{ marginTop: 15, marginBottom: 40, alignSelf: "center" }}
|
||||
hitSlop={{left: 30, right: 30, top: 10, bottom: 10}}
|
||||
|
||||
@ -1,30 +1,23 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState, useTransition} from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
} from 'react-native';
|
||||
import React, {useCallback, useMemo, useRef} from 'react';
|
||||
import {Dimensions, StyleSheet, Text, TouchableOpacity, View,} from 'react-native';
|
||||
import {
|
||||
addDays,
|
||||
addMonths,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
format,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
isWithinInterval,
|
||||
startOfMonth,
|
||||
addMonths, startOfWeek, isWithinInterval,
|
||||
} from 'date-fns';
|
||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||
import {useAtom, useSetAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
import * as Device from "expo-device";
|
||||
import debounce from "debounce";
|
||||
import {CalendarController} from "@/components/pages/calendar/CalendarController";
|
||||
|
||||
interface CalendarEvent {
|
||||
id: string;
|
||||
@ -40,12 +33,12 @@ interface CustomMonthCalendarProps {
|
||||
|
||||
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const MAX_VISIBLE_EVENTS = 3;
|
||||
const CENTER_MONTH_INDEX = 24;
|
||||
const CENTER_MONTH_INDEX = 12;
|
||||
|
||||
|
||||
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.event, {backgroundColor: event.color || '#6200ee'}]}
|
||||
style={[styles.event, {backgroundColor: event?.eventColor || '#6200ee'}]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={styles.eventText} numberOfLines={1}>
|
||||
@ -78,7 +71,7 @@ const MultiDayEvent = React.memo(({
|
||||
const style = {
|
||||
position: 'absolute' as const,
|
||||
height: 14,
|
||||
backgroundColor: event.color || '#6200ee',
|
||||
backgroundColor: event?.eventColor || '#6200ee',
|
||||
padding: 2,
|
||||
zIndex: 1,
|
||||
left: isStart ? 4 : -0.5, // Extend slightly into the border
|
||||
@ -103,22 +96,21 @@ const MultiDayEvent = React.memo(({
|
||||
);
|
||||
});
|
||||
|
||||
const Day = React.memo(({
|
||||
date,
|
||||
selectedDate,
|
||||
events,
|
||||
multiDayEvents,
|
||||
dayWidth,
|
||||
onPress
|
||||
}: {
|
||||
date: Date;
|
||||
selectedDate: Date;
|
||||
events: CalendarEvent[];
|
||||
multiDayEvents: CalendarEvent[];
|
||||
dayWidth: number;
|
||||
onPress: (date: Date) => void;
|
||||
}) => {
|
||||
const isCurrentMonth = isSameMonth(date, selectedDate);
|
||||
const Day = React.memo((
|
||||
{
|
||||
date,
|
||||
events,
|
||||
multiDayEvents,
|
||||
dayWidth,
|
||||
onPress
|
||||
}: {
|
||||
date: Date;
|
||||
events: CalendarEvent[];
|
||||
multiDayEvents: CalendarEvent[];
|
||||
dayWidth: number;
|
||||
onPress: (date: Date) => void;
|
||||
}) => {
|
||||
const isCurrentMonth = isSameMonth(date, new Date());
|
||||
const isToday = isSameDay(date, new Date());
|
||||
|
||||
const remainingSlots = Math.max(0, MAX_VISIBLE_EVENTS - multiDayEvents.length);
|
||||
@ -126,7 +118,6 @@ const Day = React.memo(({
|
||||
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
|
||||
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
|
||||
|
||||
// Calculate space needed for multi-day events
|
||||
const maxMultiDayPosition = multiDayEvents.length > 0
|
||||
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
|
||||
: 0;
|
||||
@ -140,7 +131,7 @@ const Day = React.memo(({
|
||||
>
|
||||
<View style={[
|
||||
styles.dateContainer,
|
||||
isToday && styles.todayContainer,
|
||||
isToday && {backgroundColor: events?.[0]?.eventColor},
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.dateText,
|
||||
@ -185,18 +176,9 @@ const Day = React.memo(({
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const findFirstAvailablePosition = (usedPositions: number[]): number => {
|
||||
let position = 0;
|
||||
while (usedPositions.includes(position)) {
|
||||
position++;
|
||||
}
|
||||
return position;
|
||||
};
|
||||
|
||||
export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
const {data: rawEvents} = useGetEvents();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const setSelectedDate = useSetAtom(selectedDateAtom);
|
||||
const setMode = useSetAtom(modeAtom);
|
||||
const {profileData} = useAuthContext();
|
||||
|
||||
@ -204,10 +186,8 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width;
|
||||
const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
|
||||
const dayWidth = (screenWidth - 32) / 7;
|
||||
const centerMonth = useRef(selectedDate);
|
||||
const isScrolling = useRef(false);
|
||||
const lastScrollUpdate = useRef<Date>(new Date());
|
||||
const dayWidth = screenWidth / 7;
|
||||
const centerMonth = useRef(new Date());
|
||||
|
||||
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
|
||||
|
||||
@ -255,10 +235,10 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
});
|
||||
}
|
||||
return months;
|
||||
}, [getMonthData]);
|
||||
}, [getMonthData, rawEvents]);
|
||||
|
||||
const processedEvents = useMemo(() => {
|
||||
if (!rawEvents?.length || !selectedDate) return {
|
||||
if (!rawEvents?.length) return {
|
||||
eventMap: new Map(),
|
||||
multiDayEvents: []
|
||||
};
|
||||
@ -324,7 +304,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
<Month
|
||||
date={item.date}
|
||||
days={item.days}
|
||||
selectedDate={selectedDate}
|
||||
getEventsForDay={getEventsForDay}
|
||||
getMultiDayEventsForDay={getMultiDayEventsForDay}
|
||||
dayWidth={dayWidth}
|
||||
@ -334,31 +313,12 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
/>
|
||||
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
|
||||
|
||||
const debouncedSetSelectedDate = useMemo(
|
||||
() => debounce(setSelectedDate, 500),
|
||||
[setSelectedDate]
|
||||
);
|
||||
|
||||
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (isScrolling.current) return;
|
||||
|
||||
const currentMonthIndex = Math.round(event.nativeEvent.contentOffset.x / screenWidth);
|
||||
const currentMonth = monthsToRender[currentMonthIndex];
|
||||
|
||||
if (currentMonth) {
|
||||
setSelectedDate(currentMonth.date);
|
||||
centerMonth.current = currentMonth.date;
|
||||
}
|
||||
}, [screenWidth, setSelectedDate, monthsToRender]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSetSelectedDate.clear();
|
||||
};
|
||||
}, [debouncedSetSelectedDate]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<CalendarController
|
||||
scrollViewRef={scrollViewRef}
|
||||
centerMonthIndex={CENTER_MONTH_INDEX}
|
||||
/>
|
||||
<FlashList
|
||||
ref={scrollViewRef}
|
||||
data={monthsToRender}
|
||||
@ -368,7 +328,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
initialScrollIndex={CENTER_MONTH_INDEX}
|
||||
onScroll={onScroll}
|
||||
removeClippedSubviews={true}
|
||||
estimatedItemSize={screenWidth}
|
||||
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
|
||||
@ -391,7 +350,6 @@ const keyExtractor = (item: MonthData, index: number) => `month-${index}`;
|
||||
const Month = React.memo(({
|
||||
date,
|
||||
days,
|
||||
selectedDate,
|
||||
getEventsForDay,
|
||||
getMultiDayEventsForDay,
|
||||
dayWidth,
|
||||
@ -401,7 +359,6 @@ const Month = React.memo(({
|
||||
}: {
|
||||
date: Date;
|
||||
days: Date[];
|
||||
selectedDate: Date;
|
||||
getEventsForDay: (date: Date) => CalendarEvent[];
|
||||
getMultiDayEventsForDay: (date: Date) => CalendarEvent[];
|
||||
dayWidth: number;
|
||||
@ -481,7 +438,6 @@ const Month = React.memo(({
|
||||
<Day
|
||||
key={`${weekIndex}-${dayIndex}`}
|
||||
date={date}
|
||||
selectedDate={selectedDate}
|
||||
events={getEventsForDay(date)}
|
||||
multiDayEvents={multiDayEvents}
|
||||
dayWidth={dayWidth}
|
||||
@ -529,11 +485,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
day: {
|
||||
height: '14%',
|
||||
padding: 0, // Remove padding
|
||||
padding: 0,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#eee',
|
||||
position: 'relative',
|
||||
overflow: 'visible', // Allow events to overflow
|
||||
overflow: 'visible',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
@ -555,7 +511,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center'
|
||||
// justifyContent: 'center'
|
||||
},
|
||||
weekDay: {
|
||||
alignItems: 'center',
|
||||
@ -609,6 +565,8 @@ const styles = StyleSheet.create({
|
||||
weekDayRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 16,
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default MonthCalendar;
|
||||
@ -46,7 +46,7 @@ const createEventHash = (event: FormattedEvent): string => {
|
||||
|
||||
// Precompute time constants
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
const PERIOD_IN_MS = 5 * DAY_IN_MS;
|
||||
const PERIOD_IN_MS = 180 * DAY_IN_MS;
|
||||
const TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Memoize date range calculations
|
||||
|
||||
@ -52,7 +52,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
||||
addChoreDialogProps.selectedTodo ?? defaultTodo
|
||||
);
|
||||
const [selectedAssignees, setSelectedAssignees] = useState<string[]>(
|
||||
addChoreDialogProps?.selectedTodo?.assignees ?? [user?.uid]
|
||||
addChoreDialogProps?.selectedTodo?.assignees ?? [user?.uid!]
|
||||
);
|
||||
const {width} = Dimensions.get("screen");
|
||||
const [points, setPoints] = useState<number>(todo.points);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { TouchableOpacity, Animated, Easing } from 'react-native';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
|
||||
interface RefreshButtonProps {
|
||||
onRefresh: () => Promise<void>;
|
||||
@ -9,12 +9,12 @@ interface RefreshButtonProps {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const RefreshButton = ({
|
||||
onRefresh,
|
||||
isSyncing,
|
||||
size = 24,
|
||||
color = "#83807F"
|
||||
}: RefreshButtonProps) => {
|
||||
const RefreshButton = ({
|
||||
onRefresh,
|
||||
isSyncing,
|
||||
size = 24,
|
||||
color = "#83807F"
|
||||
}: RefreshButtonProps) => {
|
||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||
const rotationLoop = useRef<Animated.CompositeAnimation | null>(null);
|
||||
|
||||
@ -29,12 +29,12 @@ const RefreshButton = ({
|
||||
const startContinuousRotation = () => {
|
||||
rotateAnim.setValue(0);
|
||||
rotationLoop.current = Animated.loop(
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
);
|
||||
rotationLoop.current.start();
|
||||
};
|
||||
@ -56,11 +56,28 @@ const RefreshButton = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handlePress} disabled={isSyncing}>
|
||||
<Animated.View style={{ transform: [{ rotate }] }}>
|
||||
<Feather name="refresh-cw" size={size} color={color} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
disabled={isSyncing}
|
||||
style={{
|
||||
width: size * 2,
|
||||
height: size + 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={{
|
||||
transform: [{ rotate }],
|
||||
width: size,
|
||||
height: size,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Feather name="refresh-cw" size={size} color={color} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user