mirror of
https://github.com/urosran/cally.git
synced 2026-03-10 21:11:44 +00:00
refresh button, calendar margins
This commit is contained in:
@ -1,8 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Drawer } from "expo-router/drawer";
|
import { Drawer } from "expo-router/drawer";
|
||||||
import { useSignOut } from "@/hooks/firebase/useSignOut";
|
import { useSignOut } from "@/hooks/firebase/useSignOut";
|
||||||
import {DrawerContentScrollView, DrawerNavigationOptions, DrawerNavigationProp} from "@react-navigation/drawer";
|
import {
|
||||||
import {Button, ButtonSize, Text, TouchableOpacity, View,} from "react-native-ui-lib";
|
DrawerContentScrollView,
|
||||||
|
DrawerNavigationOptions,
|
||||||
|
DrawerNavigationProp,
|
||||||
|
} from "@react-navigation/drawer";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonSize,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native-ui-lib";
|
||||||
import { ImageBackground, StyleSheet } from "react-native";
|
import { ImageBackground, StyleSheet } from "react-native";
|
||||||
import DrawerButton from "@/components/shared/DrawerButton";
|
import DrawerButton from "@/components/shared/DrawerButton";
|
||||||
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
||||||
@ -24,6 +34,8 @@ import {DeviceType} from "expo-device";
|
|||||||
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
||||||
import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
||||||
import { RouteProp } from "@react-navigation/core";
|
import { RouteProp } from "@react-navigation/core";
|
||||||
|
import RefreshButton from "@/components/shared/RefreshButton";
|
||||||
|
import { useCalSync } from "@/hooks/useCalSync";
|
||||||
|
|
||||||
type DrawerParamList = {
|
type DrawerParamList = {
|
||||||
index: undefined;
|
index: undefined;
|
||||||
@ -43,12 +55,11 @@ interface HeaderRightProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MemoizedViewSwitch = React.memo<ViewSwitchProps>(({ navigation }) => (
|
const MemoizedViewSwitch = React.memo<ViewSwitchProps>(({ navigation }) => (
|
||||||
<View marginR-16>
|
|
||||||
<ViewSwitch navigation={navigation} />
|
<ViewSwitch navigation={navigation} />
|
||||||
</View>
|
|
||||||
));
|
));
|
||||||
|
|
||||||
const HeaderRight = React.memo<HeaderRightProps>(({routeName, navigation}) => {
|
const HeaderRight = React.memo<HeaderRightProps>(
|
||||||
|
({ routeName, navigation }) => {
|
||||||
const showViewSwitch = ["calendar", "todos", "index"].includes(routeName);
|
const showViewSwitch = ["calendar", "todos", "index"].includes(routeName);
|
||||||
|
|
||||||
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||||
@ -56,13 +67,23 @@ const HeaderRight = React.memo<HeaderRightProps>(({routeName, navigation}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <MemoizedViewSwitch navigation={navigation} />;
|
return <MemoizedViewSwitch navigation={navigation} />;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const { mutateAsync: signOut } = useSignOut();
|
const { mutateAsync: signOut } = useSignOut();
|
||||||
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
const setUserView = useSetAtom(userSettingsView);
|
const setUserView = useSetAtom(userSettingsView);
|
||||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||||
|
const { resyncAllCalendars, isSyncing } = useCalSync();
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await resyncAllCalendars();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Refresh failed:", error);
|
||||||
|
}
|
||||||
|
}, [resyncAllCalendars]);
|
||||||
|
|
||||||
const screenOptions = ({
|
const screenOptions = ({
|
||||||
navigation,
|
navigation,
|
||||||
@ -72,7 +93,8 @@ export default function TabLayout() {
|
|||||||
route: RouteProp<DrawerParamList>;
|
route: RouteProp<DrawerParamList>;
|
||||||
}): DrawerNavigationOptions => ({
|
}): DrawerNavigationOptions => ({
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerTitleAlign: Device.deviceType === DeviceType.TABLET ? "left" : "center",
|
headerTitleAlign:
|
||||||
|
Device.deviceType === DeviceType.TABLET ? "left" : "center",
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
fontFamily: "Manrope_600SemiBold",
|
fontFamily: "Manrope_600SemiBold",
|
||||||
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
||||||
@ -86,20 +108,31 @@ export default function TabLayout() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
headerRight: () => {
|
headerRight: () => {
|
||||||
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
|
const showViewSwitch = ["calendar", "todos", "index"].includes(
|
||||||
|
route.name
|
||||||
|
);
|
||||||
|
|
||||||
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||||
return null;
|
return (
|
||||||
|
<View marginR-16>
|
||||||
|
<RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <MemoizedViewSwitch navigation={navigation}/>;
|
return (
|
||||||
|
<View marginR-16 row>
|
||||||
|
<RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} />
|
||||||
|
<MemoizedViewSwitch navigation={navigation} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
drawerStyle: {
|
drawerStyle: {
|
||||||
width: Device.deviceType === DeviceType.TABLET ? "30%" : "90%",
|
width: Device.deviceType === DeviceType.TABLET ? "30%" : "90%",
|
||||||
backgroundColor: "#f9f8f7",
|
backgroundColor: "#f9f8f7",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|||||||
@ -61,7 +61,6 @@ export default function Screen() {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
paddingRight: 200,
|
paddingRight: 200,
|
||||||
}}
|
}}
|
||||||
refreshControl={refreshControl}
|
|
||||||
bounces={true}
|
bounces={true}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
pointerEvents={isSyncing ? "auto" : "none"}
|
pointerEvents={isSyncing ? "auto" : "none"}
|
||||||
@ -74,7 +73,6 @@ export default function Screen() {
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
style={{flex: 1, height: "100%"}}
|
style={{flex: 1, height: "100%"}}
|
||||||
contentContainerStyle={{flex: 1, height: "100%"}}
|
contentContainerStyle={{flex: 1, height: "100%"}}
|
||||||
refreshControl={refreshControl}
|
|
||||||
bounces={true}
|
bounces={true}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
|
|||||||
20
assets/svgs/CheckmarkIcon.tsx
Normal file
20
assets/svgs/CheckmarkIcon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import Svg, { SvgProps, Path } from "react-native-svg"
|
||||||
|
const CheckmarkIcon = (props: SvgProps) => (
|
||||||
|
<Svg
|
||||||
|
width={13}
|
||||||
|
height={10}
|
||||||
|
viewBox="0 0 13 10"
|
||||||
|
fill={props.color || "white"}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Path
|
||||||
|
stroke="#fff"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.95}
|
||||||
|
d="m1.48 5.489 3.2 3.178 7.2-7.15"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
export default CheckmarkIcon
|
||||||
@ -5,9 +5,9 @@ import { InnerCalendar } from "@/components/pages/calendar/InnerCalendar";
|
|||||||
export default function CalendarPage() {
|
export default function CalendarPage() {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{ flex: 1, height: "100%", padding: 10 }}
|
style={{ flex: 1, height: "100%", padding: 0 }}
|
||||||
paddingH-22
|
paddingH-0
|
||||||
paddingT-22
|
paddingT-0
|
||||||
>
|
>
|
||||||
{/*<HeaderTemplate
|
{/*<HeaderTemplate
|
||||||
message={"Let's get your week started !"}
|
message={"Let's get your week started !"}
|
||||||
|
|||||||
@ -25,9 +25,10 @@ export const InnerCalendar = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View
|
<View
|
||||||
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30, marginBottom: 10, overflow: "hidden"}}
|
style={{flex: 1, backgroundColor: "#fff", borderRadius: 0, marginBottom: 0, overflow: "hidden"}}
|
||||||
ref={calendarContainerRef}
|
ref={calendarContainerRef}
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
|
paddingB-15
|
||||||
>
|
>
|
||||||
<CalendarHeader/>
|
<CalendarHeader/>
|
||||||
{calendarHeight > 0 && (
|
{calendarHeight > 0 && (
|
||||||
|
|||||||
67
components/shared/RefreshButton.tsx
Normal file
67
components/shared/RefreshButton.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import { TouchableOpacity, Animated, Easing } from 'react-native';
|
||||||
|
import { Feather } from '@expo/vector-icons';
|
||||||
|
|
||||||
|
interface RefreshButtonProps {
|
||||||
|
onRefresh: () => Promise<void>;
|
||||||
|
isSyncing: boolean;
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RefreshButton = ({
|
||||||
|
onRefresh,
|
||||||
|
isSyncing,
|
||||||
|
size = 24,
|
||||||
|
color = "#83807F"
|
||||||
|
}: RefreshButtonProps) => {
|
||||||
|
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
const rotationLoop = useRef<Animated.CompositeAnimation | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSyncing) {
|
||||||
|
startContinuousRotation();
|
||||||
|
} else {
|
||||||
|
stopRotation();
|
||||||
|
}
|
||||||
|
}, [isSyncing]);
|
||||||
|
|
||||||
|
const startContinuousRotation = () => {
|
||||||
|
rotateAnim.setValue(0);
|
||||||
|
rotationLoop.current = Animated.loop(
|
||||||
|
Animated.timing(rotateAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 1000,
|
||||||
|
easing: Easing.linear,
|
||||||
|
useNativeDriver: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
rotationLoop.current.start();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRotation = () => {
|
||||||
|
rotationLoop.current?.stop();
|
||||||
|
rotateAnim.setValue(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotate = rotateAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: ['0deg', '360deg'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePress = async () => {
|
||||||
|
if (!isSyncing) {
|
||||||
|
await onRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={handlePress} disabled={isSyncing}>
|
||||||
|
<Animated.View style={{ transform: [{ rotate }] }}>
|
||||||
|
<Feather name="refresh-cw" size={size} color={color} />
|
||||||
|
</Animated.View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RefreshButton;
|
||||||
Reference in New Issue
Block a user