Calendar page refactor

This commit is contained in:
Milan Paunovic
2024-10-19 17:20:34 +02:00
parent d2b46ad977
commit 3f7fc92952
21 changed files with 1580 additions and 1755 deletions

View File

@ -1,14 +1,11 @@
import React from "react"; import React from "react";
import { CalendarProvider } from "@/contexts/CalendarContext"; // Import the new CalendarPage component
import CalendarPage from "@/components/pages/calendar/CalendarPage"; import CalendarPage from "@/components/pages/calendar/CalendarPage";
import { SettingsContextProvider } from "@/contexts/SettingsContext"; import {SettingsContextProvider} from "@/contexts/SettingsContext";
export default function Screen() { export default function Screen() {
return ( return (
<SettingsContextProvider> <SettingsContextProvider>
<CalendarProvider> <CalendarPage/>
<CalendarPage /> </SettingsContextProvider>
</CalendarProvider> );
</SettingsContextProvider>
);
} }

View File

@ -1,170 +1,6 @@
import React, { useEffect } from "react"; import React, {useEffect} from "react";
import { DefaultTheme, ThemeProvider } from "@react-navigation/native"; import {DefaultTheme, ThemeProvider} from "@react-navigation/native";
import { import {
useFonts,
Manrope_200ExtraLight,
Manrope_300Light,
Manrope_400Regular,
Manrope_500Medium,
Manrope_600SemiBold,
Manrope_700Bold,
Manrope_800ExtraBold,
} from "@expo-google-fonts/manrope";
import {
PlusJakartaSans_200ExtraLight,
PlusJakartaSans_300Light,
PlusJakartaSans_400Regular,
PlusJakartaSans_500Medium,
PlusJakartaSans_600SemiBold,
PlusJakartaSans_700Bold,
PlusJakartaSans_800ExtraBold,
PlusJakartaSans_200ExtraLight_Italic,
PlusJakartaSans_300Light_Italic,
PlusJakartaSans_400Regular_Italic,
PlusJakartaSans_500Medium_Italic,
PlusJakartaSans_600SemiBold_Italic,
PlusJakartaSans_700Bold_Italic,
PlusJakartaSans_800ExtraBold_Italic,
} from "@expo-google-fonts/plus-jakarta-sans";
import {
Poppins_100Thin,
Poppins_100Thin_Italic,
Poppins_200ExtraLight,
Poppins_200ExtraLight_Italic,
Poppins_300Light,
Poppins_300Light_Italic,
Poppins_400Regular,
Poppins_400Regular_Italic,
Poppins_500Medium,
Poppins_500Medium_Italic,
Poppins_600SemiBold,
Poppins_600SemiBold_Italic,
Poppins_700Bold,
Poppins_700Bold_Italic,
Poppins_800ExtraBold,
Poppins_800ExtraBold_Italic,
Poppins_900Black,
Poppins_900Black_Italic,
} from "@expo-google-fonts/poppins";
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 {
ThemeManager,
Typography,
Toast,
TextProps,
} from "react-native-ui-lib";
import functions from "@react-native-firebase/functions";
import auth from "@react-native-firebase/auth";
import firestore from "@react-native-firebase/firestore";
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient();
if (__DEV__) {
functions().useEmulator("localhost", 5001);
firestore().useEmulator("localhost", 5471);
auth().useEmulator("http://localhost:9099");
}
type TextStyleBase =
| "text10"
| "text20"
| "text30"
| "text40"
| "text50"
| "text60"
| "text70"
| "text80"
| "text90"
| "text100";
type TextStyleModifier = "R" | "M" | "BO" | "H" | "BL" | "L";
type TextStyle = TextStyleBase | `${TextStyleBase}${TextStyleModifier}`;
type TextStyleProps = {
[K in TextStyle]?: boolean;
};
type ExtendedTextProps = TextProps & TextStyleProps;
interface FontStyle {
fontFamily: string;
fontSize: number;
}
const getManropeFontStyle = (style: TextStyle): FontStyle => {
let fontFamily: string;
let fontSize: number;
if (style.includes("L") || style.includes("BL"))
fontFamily = "Manrope_300Light";
else if (style.includes("R")) fontFamily = "Manrope_400Regular";
else if (style.includes("M")) fontFamily = "Manrope_500Medium";
else if (style.includes("BO") || style.includes("H"))
fontFamily = "Manrope_700Bold";
else {
const baseStyle = style.slice(0, 6) as TextStyleBase;
switch (baseStyle) {
case "text10":
case "text20":
fontFamily = "Manrope_700Bold";
break;
case "text30":
case "text40":
fontFamily = "Manrope_600SemiBold";
break;
case "text50":
fontFamily = "Manrope_400Regular";
break;
default:
fontFamily = "Manrope_300Light";
}
}
switch (style.slice(0, 6) as TextStyleBase) {
case "text10":
fontSize = 64;
break;
case "text20":
fontSize = 50;
break;
case "text30":
fontSize = 36;
break;
case "text40":
fontSize = 28;
break;
case "text50":
fontSize = 24;
break;
case "text60":
fontSize = 20;
break;
case "text70":
fontSize = 16;
break;
case "text80":
fontSize = 14;
break;
case "text90":
fontSize = 12;
break;
case "text100":
fontSize = 10;
break;
default:
fontSize = 16;
}
return { fontFamily, fontSize };
};
export default function RootLayout() {
const [loaded] = useFonts({
Manrope_200ExtraLight, Manrope_200ExtraLight,
Manrope_300Light, Manrope_300Light,
Manrope_400Regular, Manrope_400Regular,
@ -172,20 +8,25 @@ export default function RootLayout() {
Manrope_600SemiBold, Manrope_600SemiBold,
Manrope_700Bold, Manrope_700Bold,
Manrope_800ExtraBold, Manrope_800ExtraBold,
useFonts,
} from "@expo-google-fonts/manrope";
import {
PlusJakartaSans_200ExtraLight, PlusJakartaSans_200ExtraLight,
PlusJakartaSans_300Light,
PlusJakartaSans_400Regular,
PlusJakartaSans_500Medium,
PlusJakartaSans_600SemiBold,
PlusJakartaSans_700Bold,
PlusJakartaSans_800ExtraBold,
PlusJakartaSans_200ExtraLight_Italic, PlusJakartaSans_200ExtraLight_Italic,
PlusJakartaSans_300Light,
PlusJakartaSans_300Light_Italic, PlusJakartaSans_300Light_Italic,
PlusJakartaSans_400Regular,
PlusJakartaSans_400Regular_Italic, PlusJakartaSans_400Regular_Italic,
PlusJakartaSans_500Medium,
PlusJakartaSans_500Medium_Italic, PlusJakartaSans_500Medium_Italic,
PlusJakartaSans_600SemiBold,
PlusJakartaSans_600SemiBold_Italic, PlusJakartaSans_600SemiBold_Italic,
PlusJakartaSans_700Bold,
PlusJakartaSans_700Bold_Italic, PlusJakartaSans_700Bold_Italic,
PlusJakartaSans_800ExtraBold,
PlusJakartaSans_800ExtraBold_Italic, PlusJakartaSans_800ExtraBold_Italic,
} from "@expo-google-fonts/plus-jakarta-sans";
import {
Poppins_100Thin, Poppins_100Thin,
Poppins_100Thin_Italic, Poppins_100Thin_Italic,
Poppins_200ExtraLight, Poppins_200ExtraLight,
@ -204,72 +45,223 @@ export default function RootLayout() {
Poppins_800ExtraBold_Italic, Poppins_800ExtraBold_Italic,
Poppins_900Black, Poppins_900Black,
Poppins_900Black_Italic, Poppins_900Black_Italic,
}); } from "@expo-google-fonts/poppins";
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 {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
useEffect(() => { SplashScreen.preventAutoHideAsync();
if (loaded) {
SplashScreen.hideAsync();
const typographies: Partial<Record<TextStyle, FontStyle>> = {}; const queryClient = new QueryClient();
(
[
"text10",
"text20",
"text30",
"text40",
"text50",
"text60",
"text70",
"text80",
"text90",
"text100",
] as const
).forEach((baseStyle) => {
typographies[baseStyle] = getManropeFontStyle(baseStyle);
(["R", "M", "BO", "H", "BL", "L"] as const).forEach((modifier) => {
const style = `${baseStyle}${modifier}` as TextStyle;
typographies[style] = getManropeFontStyle(style);
});
});
Typography.loadTypographies(typographies); if (__DEV__) {
// functions().useEmulator("localhost", 5001);
ThemeManager.setComponentTheme( // firestore().useEmulator("localhost", 5471);
"Text", // auth().useEmulator("http://localhost:9099");
(props: ExtendedTextProps, context: unknown) => { }
const textStyle = (
Object.keys(props) as Array<keyof ExtendedTextProps> type TextStyleBase =
).find((key) => typographies[key as TextStyle]) as | "text10"
| TextStyle | "text20"
| undefined; | "text30"
| "text40"
return { | "text50"
style: [ | "text60"
Typography.text50, | "text70"
textStyle ? typographies[textStyle] : undefined, | "text80"
], | "text90"
}; | "text100";
} type TextStyleModifier = "R" | "M" | "BO" | "H" | "BL" | "L";
); type TextStyle = TextStyleBase | `${TextStyleBase}${TextStyleModifier}`;
}
}, [loaded]); type TextStyleProps = {
[K in TextStyle]?: boolean;
if (!loaded) { };
return null;
} type ExtendedTextProps = TextProps & TextStyleProps;
return ( interface FontStyle {
<QueryClientProvider client={queryClient}> fontFamily: string;
<AuthContextProvider> fontSize: number;
<ThemeProvider value={DefaultTheme}> }
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} /> const getManropeFontStyle = (style: TextStyle): FontStyle => {
<Stack.Screen name="(unauth)" options={{ headerShown: false }} /> let fontFamily: string;
<Stack.Screen name="+not-found" /> let fontSize: number;
</Stack>
<Toast /> if (style.includes("L") || style.includes("BL"))
</ThemeProvider> fontFamily = "Manrope_300Light";
</AuthContextProvider> else if (style.includes("R")) fontFamily = "Manrope_400Regular";
</QueryClientProvider> else if (style.includes("M")) fontFamily = "Manrope_500Medium";
); else if (style.includes("BO") || style.includes("H"))
fontFamily = "Manrope_700Bold";
else {
const baseStyle = style.slice(0, 6) as TextStyleBase;
switch (baseStyle) {
case "text10":
case "text20":
fontFamily = "Manrope_700Bold";
break;
case "text30":
case "text40":
fontFamily = "Manrope_600SemiBold";
break;
case "text50":
fontFamily = "Manrope_400Regular";
break;
default:
fontFamily = "Manrope_300Light";
}
}
switch (style.slice(0, 6) as TextStyleBase) {
case "text10":
fontSize = 64;
break;
case "text20":
fontSize = 50;
break;
case "text30":
fontSize = 36;
break;
case "text40":
fontSize = 28;
break;
case "text50":
fontSize = 24;
break;
case "text60":
fontSize = 20;
break;
case "text70":
fontSize = 16;
break;
case "text80":
fontSize = 14;
break;
case "text90":
fontSize = 12;
break;
case "text100":
fontSize = 10;
break;
default:
fontSize = 16;
}
return {fontFamily, fontSize};
};
export default function RootLayout() {
const [loaded] = useFonts({
Manrope_200ExtraLight,
Manrope_300Light,
Manrope_400Regular,
Manrope_500Medium,
Manrope_600SemiBold,
Manrope_700Bold,
Manrope_800ExtraBold,
PlusJakartaSans_200ExtraLight,
PlusJakartaSans_300Light,
PlusJakartaSans_400Regular,
PlusJakartaSans_500Medium,
PlusJakartaSans_600SemiBold,
PlusJakartaSans_700Bold,
PlusJakartaSans_800ExtraBold,
PlusJakartaSans_200ExtraLight_Italic,
PlusJakartaSans_300Light_Italic,
PlusJakartaSans_400Regular_Italic,
PlusJakartaSans_500Medium_Italic,
PlusJakartaSans_600SemiBold_Italic,
PlusJakartaSans_700Bold_Italic,
PlusJakartaSans_800ExtraBold_Italic,
Poppins_100Thin,
Poppins_100Thin_Italic,
Poppins_200ExtraLight,
Poppins_200ExtraLight_Italic,
Poppins_300Light,
Poppins_300Light_Italic,
Poppins_400Regular,
Poppins_400Regular_Italic,
Poppins_500Medium,
Poppins_500Medium_Italic,
Poppins_600SemiBold,
Poppins_600SemiBold_Italic,
Poppins_700Bold,
Poppins_700Bold_Italic,
Poppins_800ExtraBold,
Poppins_800ExtraBold_Italic,
Poppins_900Black,
Poppins_900Black_Italic,
});
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
const typographies: Partial<Record<TextStyle, FontStyle>> = {};
(
[
"text10",
"text20",
"text30",
"text40",
"text50",
"text60",
"text70",
"text80",
"text90",
"text100",
] as const
).forEach((baseStyle) => {
typographies[baseStyle] = getManropeFontStyle(baseStyle);
(["R", "M", "BO", "H", "BL", "L"] as const).forEach((modifier) => {
const style = `${baseStyle}${modifier}` as TextStyle;
typographies[style] = getManropeFontStyle(style);
});
});
Typography.loadTypographies(typographies);
ThemeManager.setComponentTheme(
"Text",
(props: ExtendedTextProps) => {
const textStyle = (
Object.keys(props) as Array<keyof ExtendedTextProps>
).find((key) => typographies[key as TextStyle]) as
| TextStyle
| undefined;
return {
style: [
Typography.text50,
textStyle ? typographies[textStyle] : undefined,
],
};
}
);
}
}, [loaded]);
if (!loaded) {
return null;
}
return (
<QueryClientProvider client={queryClient}>
<AuthContextProvider>
<ThemeProvider value={DefaultTheme}>
<Stack>
<Stack.Screen name="(auth)" options={{headerShown: false}}/>
<Stack.Screen name="(unauth)" options={{headerShown: false}}/>
<Stack.Screen name="+not-found"/>
</Stack>
<Toast/>
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
);
} }

View File

@ -1,188 +1,172 @@
import React, { useState } from "react"; import React, {useState} from "react";
import { import {MaterialIcons,} from "@expo/vector-icons";
AntDesign, import {Button, Card, Dialog, PanningProvider, Text, View,} from "react-native-ui-lib";
Feather, import {StyleSheet, TouchableOpacity} from "react-native";
MaterialCommunityIcons,
MaterialIcons,
} from "@expo/vector-icons";
import {
Button,
ButtonSize,
Card,
Dialog,
PanningProvider,
Text,
View,
} from "react-native-ui-lib";
import { StyleSheet, TouchableOpacity } from "react-native";
import { ManuallyAddEventModal } from "@/components/pages/calendar/ManuallyAddEventModal";
import AddChoreDialog from "../todos/AddChoreDialog"; import AddChoreDialog from "../todos/AddChoreDialog";
import { ToDosContextProvider } from "@/contexts/ToDosContext"; import {ToDosContextProvider} from "@/contexts/ToDosContext";
import UploadImageDialog from "./UploadImageDialog"; import UploadImageDialog from "./UploadImageDialog";
import CameraIcon from "@/assets/svgs/CameraIcon"; import CameraIcon from "@/assets/svgs/CameraIcon";
import CalendarIcon from "@/assets/svgs/CalendarIcon"; import CalendarIcon from "@/assets/svgs/CalendarIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon"; import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import {useSetAtom} from "jotai";
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
export const AddEventDialog = () => { export const AddEventDialog = () => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [showManualInputModal, setShowManualInputModal] = useState(false); const [choreDialogVisible, setChoreDialogVisible] = useState<boolean>(false);
const [choreDialogVisible, setChoreDialogVisible] = useState<boolean>(false); const [showUploadDialog, setShowUploadDialog] = useState<boolean>(false);
const [showUploadDialog, setShowUploadDialog] = useState<boolean>(false); const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom)
const handleOpenManualInputModal = () => { const handleOpenManualInputModal = () => {
setShow(false); setShow(false);
setTimeout(() => { setTimeout(() => {
setShowManualInputModal(true); setSelectedNewEndDate(new Date());
}, 500); }, 500);
}; };
const handleScanImageDialog = () => { const handleScanImageDialog = () => {
setShow(false); setShow(false);
setTimeout(() => { setTimeout(() => {
setShowUploadDialog(true); setShowUploadDialog(true);
}, 100); }, 100);
}; };
return ( return (
<ToDosContextProvider> <ToDosContextProvider>
<> <>
<Button <Button
style={{ style={{
position: "absolute", position: "absolute",
bottom: 20, bottom: 20,
right: 20, right: 20,
height: 40, height: 40,
borderRadius: 30, borderRadius: 30,
backgroundColor: "#fd1775", backgroundColor: "#fd1775",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}} }}
color="white" color="white"
enableShadow enableShadow
onPress={() => setShow(true)} onPress={() => setShow(true)}
> >
<View row centerV centerH> <View row centerV centerH>
<MaterialIcons name="add" size={22} color={"white"} /> <MaterialIcons name="add" size={22} color={"white"}/>
<Text white style={{ fontSize: 16, fontFamily: 'Manrope_600SemiBold' }}> <Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold'}}>
New New
</Text> </Text>
</View> </View>
</Button> </Button>
<Dialog <Dialog
visible={show} visible={show}
onDismiss={() => setShow(false)} onDismiss={() => setShow(false)}
panDirection={PanningProvider.Directions.DOWN} panDirection={PanningProvider.Directions.DOWN}
center center
> >
<Card style={styles.dialogCard}> <Card style={styles.dialogCard}>
<Text text60 style={styles.modalTitle}> <Text text60 style={styles.modalTitle}>
Create a new event Create a new event
</Text> </Text>
<View <View
style={{ marginTop: 20, alignItems: "center", width: "100%" }} style={{marginTop: 20, alignItems: "center", width: "100%"}}
> >
<Button <Button
style={{ style={{
marginBottom: 10, marginBottom: 10,
backgroundColor: "#ea156c", backgroundColor: "#ea156c",
justifyContent: "center", justifyContent: "center",
width: "100%", width: "100%",
paddingVertical: 13, paddingVertical: 13,
}} }}
label="Scan Image" label="Scan Image"
labelStyle={styles.btnLabel} labelStyle={styles.btnLabel}
onPress={handleScanImageDialog} onPress={handleScanImageDialog}
iconSource={() => ( iconSource={() => (
<CameraIcon color="white" style={styles.btnIcon} /> <CameraIcon color="white" style={styles.btnIcon}/>
)} )}
/> />
<Button <Button
style={{ style={{
marginBottom: 10, marginBottom: 10,
backgroundColor: "#e28800", backgroundColor: "#e28800",
justifyContent: "center", justifyContent: "center",
width: "100%", width: "100%",
paddingVertical: 13, paddingVertical: 13,
}} }}
label="Create Event" label="Create Event"
labelStyle={styles.btnLabel} labelStyle={styles.btnLabel}
onPress={handleOpenManualInputModal} onPress={handleOpenManualInputModal}
iconSource={() => ( iconSource={() => (
<CalendarIcon color={"white"} style={styles.btnIcon} /> <CalendarIcon color={"white"} style={styles.btnIcon}/>
)} )}
/> />
<Button <Button
style={{ style={{
marginBottom: 10, marginBottom: 10,
backgroundColor: "#05a8b6", backgroundColor: "#05a8b6",
justifyContent: "center", justifyContent: "center",
width: "100%", width: "100%",
paddingVertical: 13, paddingVertical: 13,
}} }}
label="Add To Do" label="Add To Do"
labelStyle={styles.btnLabel} labelStyle={styles.btnLabel}
onPress={() => setChoreDialogVisible(true)} onPress={() => setChoreDialogVisible(true)}
iconSource={() => ( iconSource={() => (
<NavToDosIcon <NavToDosIcon
color={"white"} color={"white"}
width={23} width={23}
style={styles.btnIcon} style={styles.btnIcon}
/> />
)} )}
/> />
</View> </View>
<TouchableOpacity onPress={() => setShow(false)}> <TouchableOpacity onPress={() => setShow(false)}>
<Text style={styles.bottomText} text70> <Text style={styles.bottomText} text70>
Go back to calendar Go back to calendar
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</Card> </Card>
</Dialog> </Dialog>
<AddChoreDialog <AddChoreDialog
isVisible={choreDialogVisible} isVisible={choreDialogVisible}
setIsVisible={setChoreDialogVisible} setIsVisible={setChoreDialogVisible}
/> />
<ManuallyAddEventModal <UploadImageDialog
show={showManualInputModal} show={showUploadDialog}
close={() => setShowManualInputModal(false)} setShow={setShowUploadDialog}
/> />
<UploadImageDialog </>
show={showUploadDialog} </ToDosContextProvider>
setShow={setShowUploadDialog} );
/>
</>
</ToDosContextProvider>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
modalTitle: { modalTitle: {
fontSize: 22, fontSize: 22,
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",
marginBottom: 16, marginBottom: 16,
}, },
bottomText: { bottomText: {
marginTop: 20, marginTop: 20,
color: "#999999", color: "#999999",
fontSize: 13.53, fontSize: 13.53,
fontFamily: "Poppins_500Medium", fontFamily: "Poppins_500Medium",
}, },
dialogCard: { dialogCard: {
paddingHorizontal: 40, paddingHorizontal: 40,
paddingTop: 35, paddingTop: 35,
paddingBottom: 20, paddingBottom: 20,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
borderRadius: 20, borderRadius: 20,
}, },
btnLabel: { btnLabel: {
fontSize: 15, fontSize: 15,
fontFamily: "PlusJakartaSans_500Medium", fontFamily: "PlusJakartaSans_500Medium",
}, },
btnIcon: { marginRight: 10 }, btnIcon: {marginRight: 10},
}); });

View File

@ -0,0 +1,102 @@
import React, {memo} from 'react';
import {Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
import {MaterialIcons} from "@expo/vector-icons";
import {modeMap, months} from './constants';
import {StyleSheet} from "react-native";
import {useAtom} from "jotai";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
export const CalendarHeader = memo(() => {
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom)
const [mode, setMode] = useAtom(modeAtom)
const handleSegmentChange = (index: number) => {
const selectedMode = modeMap.get(index);
if (selectedMode) {
setMode(selectedMode as "day" | "week" | "month");
}
};
const handleMonthChange = (month: string) => {
const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear();
const newMonthIndex = months.indexOf(month);
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
setSelectedDate(updatedDate);
};
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "white",
marginBottom: 10,
}}
>
<View row centerV gap-3>
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
{selectedDate.getFullYear()}
</Text>
<Picker
value={months[selectedDate.getMonth()]}
placeholder={"Select Month"}
style={{fontFamily: "Manrope_500Medium", fontSize: 17}}
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>
<SegmentedControl
segments={[{label: "D"}, {label: "W"}, {label: "M"}]}
backgroundColor="#ececec"
inactiveColor="#919191"
activeBackgroundColor="#ea156c"
activeColor="white"
outlineColor="white"
outlineWidth={3}
segmentLabelStyle={styles.segmentslblStyle}
onChangeIndex={handleSegmentChange}
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
/>
</View>
</View>
);
});
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
calHeader: {
borderWidth: 0,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
},
});

View File

@ -1,207 +1,20 @@
import React, { useRef, useState } from "react"; import React from "react";
import { LayoutChangeEvent, StyleSheet } from "react-native"; import {View,} from "react-native-ui-lib";
import { Calendar } from "react-native-big-calendar";
import {
Picker,
PickerModes,
SegmentedControl,
View,
} from "react-native-ui-lib";
import { MaterialIcons } from "@expo/vector-icons";
import { AddEventDialog } from "@/components/pages/calendar/AddEventDialog";
import HeaderTemplate from "@/components/shared/HeaderTemplate"; import HeaderTemplate from "@/components/shared/HeaderTemplate";
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch"; import {InnerCalendar} from "@/components/pages/calendar/InnerCalendar";
import { ManuallyAddEventModal } from "@/components/pages/calendar/ManuallyAddEventModal";
import { CalendarEvent } from "@/contexts/CalendarContext";
import { useSettingsContext } from "@/contexts/SettingsContext";
import EditEventDialog from "./EditEventDialog";
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
import { Text } from "react-native-ui-lib";
const modeMap = new Map([
[0, "day"],
[1, "week"],
[2, "month"],
]);
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export default function CalendarPage() { export default function CalendarPage() {
const { calendarColor } = useSettingsContext(); return (
const [editVisible, setEditVisible] = useState<boolean>(false);
const [eventForEdit, setEventForEdit] = useState<CalendarEvent>();
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
calHeader: {
borderWidth: 0,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
},
});
const [isFamilyView, setIsFamilyView] = useState<boolean>(false);
const [calendarHeight, setCalendarHeight] = useState(0);
const [mode, setMode] = useState<"week" | "month" | "day">("week");
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [selectedNewEventDate, setSelectedNewEndDate] = useState<
Date | undefined
>(undefined);
const calendarContainerRef = useRef(null);
const { data: events } = useGetEvents(isFamilyView);
const onLayout = (event: LayoutChangeEvent) => {
const { height } = event.nativeEvent.layout;
setCalendarHeight(height);
};
const handleSegmentChange = (index: number) => {
const selectedMode = modeMap.get(index);
if (selectedMode) {
setMode(selectedMode as "day" | "week" | "month");
}
};
const handleMonthChange = (month: string) => {
const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear();
const newMonthIndex = months.indexOf(month);
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
setSelectedDate(updatedDate);
};
return (
<View
style={{ flex: 1, height: "100%", padding: 10 }}
paddingH-22
paddingT-0
>
<HeaderTemplate
message={"Let's get your week started!"}
isWelcome={true}
/>
<View
style={{ flex: 1, backgroundColor: "#fff", borderRadius: 30 }}
ref={calendarContainerRef}
onLayout={onLayout}
>
<View <View
style={{ style={{flex: 1, height: "100%", padding: 10}}
flexDirection: "row", paddingH-22
justifyContent: "space-between", paddingT-0
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "white",
marginBottom: 10,
}}
> >
<View row centerV gap-3> <HeaderTemplate
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}> message={"Let's get your week started!"}
{selectedDate.getFullYear()} isWelcome
</Text>
<Picker
value={months[selectedDate.getMonth()]} // Get the month from the date
placeholder={"Select Month"}
style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}
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>
<SegmentedControl
segments={[{ label: "D" }, { label: "W" }, { label: "M" }]}
backgroundColor="#ececec"
inactiveColor="#919191"
activeBackgroundColor="#ea156c"
activeColor="white"
outlineColor="white"
outlineWidth={3}
segmentLabelStyle={styles.segmentslblStyle}
onChangeIndex={handleSegmentChange}
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
/> />
</View> <InnerCalendar/>
</View> </View>
);
{calendarHeight > 0 && (
<Calendar
bodyContainerStyle={styles.calHeader}
mode={mode}
events={isFamilyView ? events ?? [] : events ?? []}
eventCellStyle={(event) => ({ backgroundColor: event.eventColor })}
onPressEvent={(event) => {
setEditVisible(true);
setEventForEdit(event);
}}
height={calendarHeight}
activeDate={selectedDate}
date={selectedDate}
onPressCell={setSelectedNewEndDate}
headerContentStyle={mode === "day" ? styles.dayModeHeader : {}}
onSwipeEnd={(date) => {
setSelectedDate(date);
}}
/>
)}
</View>
<CalendarViewSwitch viewSwitch={setIsFamilyView} />
<AddEventDialog />
{eventForEdit && (
<EditEventDialog
isVisible={editVisible}
setIsVisible={() => {
setEditVisible(!editVisible);
}}
event={eventForEdit}
/>
)}
<ManuallyAddEventModal
key={`${selectedNewEventDate}`}
initialDate={selectedNewEventDate}
show={!!selectedNewEventDate}
close={() => setSelectedNewEndDate(undefined)}
/>
</View>
);
} }

View File

@ -1,90 +1,90 @@
import { View, Text, Button, TouchableOpacity } from "react-native-ui-lib"; import {Text, TouchableOpacity, View} from "react-native-ui-lib";
import React, { useState } from "react"; import React, {useState} from "react";
import { MaterialIcons } from "@expo/vector-icons"; import {StyleSheet} from "react-native";
import { StyleSheet } from "react-native"; import {useSetAtom} from "jotai";
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
interface ICalendarViewProps {
viewSwitch: (value: boolean) => void;
}
const CalendarViewSwitch = (calendarViewProps: ICalendarViewProps) => {
const [calView, setCalView] = useState<boolean>(false);
return ( const CalendarViewSwitch = () => {
<View const [calView, setCalView] = useState<boolean>(false);
row const viewSwitch = useSetAtom(isFamilyViewAtom)
spread
style={{ return (
position: "absolute",
bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
// iOS shadow
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Android shadow (elevation)
elevation: 6,
}}
centerV
>
<TouchableOpacity
onPress={() => {
setCalView(true);
calendarViewProps.viewSwitch(true);
}}
>
<View <View
centerV row
centerH spread
height={40} style={{
paddingH-15 position: "absolute",
style={calView ? styles.switchBtnActive : styles.switchBtn} bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
// iOS shadow
shadowColor: "#000",
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Android shadow (elevation)
elevation: 6,
}}
centerV
> >
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}> <TouchableOpacity
Family View onPress={() => {
</Text> setCalView(true);
</View> viewSwitch(true);
</TouchableOpacity> }}
>
<View
centerV
centerH
height={40}
paddingH-15
style={calView ? styles.switchBtnActive : styles.switchBtn}
>
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
Family View
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
setCalView(false); setCalView(false);
calendarViewProps.viewSwitch(false); viewSwitch(false);
}} }}
> >
<View <View
centerV centerV
centerH centerH
height={40} height={40}
paddingH-15 paddingH-15
style={!calView ? styles.switchBtnActive : styles.switchBtn} style={!calView ? styles.switchBtnActive : styles.switchBtn}
> >
<Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}> <Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
My View My View
</Text> </Text>
</View>
</TouchableOpacity>
</View> </View>
</TouchableOpacity> );
</View>
);
}; };
export default CalendarViewSwitch; export default CalendarViewSwitch;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
switchBtnActive: { switchBtnActive: {
backgroundColor: "#a1a1a1", backgroundColor: "#a1a1a1",
borderRadius: 50, borderRadius: 50,
}, },
switchBtn: { switchBtn: {
backgroundColor: "white", backgroundColor: "white",
borderRadius: 50, borderRadius: 50,
}, },
switchTxt:{ switchTxt: {
fontSize: 16, fontSize: 16,
fontFamily: 'Manrope_600SemiBold' fontFamily: 'Manrope_600SemiBold'
} }
}); });

View File

@ -1,312 +1,303 @@
import { View, Text, Button, Switch } from "react-native-ui-lib"; import {Button, ButtonSize, DateTimePicker, Dialog, Switch, Text, TextField, View} from "react-native-ui-lib";
import React, { useEffect, useState } from "react"; import React from "react";
import { Feather, AntDesign, Ionicons } from "@expo/vector-icons"; import {AntDesign, Feather, Ionicons} from "@expo/vector-icons";
import { import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView";
Dialog, import {StyleSheet} from "react-native";
TextField,
DateTimePicker,
Picker,
ButtonSize,
} from "react-native-ui-lib";
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
import { StyleSheet } from "react-native";
import DropModalIcon from "@/assets/svgs/DropModalIcon"; import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { CalendarEvent } from "@/contexts/CalendarContext";
import ClockIcon from "@/assets/svgs/ClockIcon"; import ClockIcon from "@/assets/svgs/ClockIcon";
import LockIcon from "@/assets/svgs/LockIcon"; import LockIcon from "@/assets/svgs/LockIcon";
import MenuIcon from "@/assets/svgs/MenuIcon"; import MenuIcon from "@/assets/svgs/MenuIcon";
import { useUpdateEvent } from "@/hooks/firebase/useUpdateEvent"; import {useUpdateEvent} from "@/hooks/firebase/useUpdateEvent";
import {editVisibleAtom, eventForEditAtom} from "@/components/pages/calendar/atoms";
import {useAtom} from "jotai";
interface IEditEventDialog {
event: CalendarEvent;
isVisible: boolean;
setIsVisible: (value: boolean) => void;
}
const EditEventDialog = (editEventProps: IEditEventDialog) => {
const [event, setEvent] = useState<CalendarEvent>(editEventProps.event);
const { mutateAsync: updateEvent } = useUpdateEvent(); const EditEventDialog = () => {
const [isVisible, setIsVisible] = useAtom(editVisibleAtom)
const [event, setEvent] = useAtom(eventForEditAtom)
useEffect(() => { const {mutateAsync: updateEvent} = useUpdateEvent();
setEvent(editEventProps.event);
}, [editEventProps.isVisible]);
return ( if (!event) return null
<Dialog
bottom={true} return (
height={"90%"} <Dialog
panDirection={PanningDirectionsEnum.DOWN} bottom={true}
onDismiss={() => editEventProps.setIsVisible(false)} height={"90%"}
containerStyle={{ panDirection={PanningDirectionsEnum.DOWN}
borderRadius: 10, onDismiss={() => setIsVisible(false)}
backgroundColor: "white", containerStyle={{
width: "100%", borderRadius: 10,
alignSelf: "stretch", backgroundColor: "white",
padding: 0, width: "100%",
paddingTop: 4, alignSelf: "stretch",
margin: 0, padding: 0,
}} paddingTop: 4,
visible={editEventProps.isVisible} margin: 0,
>
<View row spread>
<Button
color="#05a8b6"
style={styles.topBtn}
label="Cancel"
onPress={() => {
editEventProps.setIsVisible(false);
}}
/>
<View marginT-12>
<DropModalIcon
onPress={() => {
editEventProps.setIsVisible(false);
}} }}
/> visible={isVisible}
</View> >
<Button <View row spread>
color="#05a8b6" <Button
style={styles.topBtn} color="#05a8b6"
label="Save" style={styles.topBtn}
onPress={() => { label="Cancel"
try { onPress={() => {
if (event.id) { setIsVisible(false);
updateEvent(event).then(() => editEventProps.setIsVisible(false)); }}
} />
} catch (error) { <View marginT-12>
console.error(error); <DropModalIcon
} onPress={() => {
}} setIsVisible(false);
/> }}
</View> />
</View>
<TextField <Button
placeholder="Edit event title" color="#05a8b6"
value={event.title} style={styles.topBtn}
onChangeText={(text) => { label="Save"
setEvent((prevEvent) => ({ onPress={() => {
...prevEvent, try {
title: text, if (event.id) {
})); updateEvent(event).then(() => setIsVisible(false));
}} }
placeholderTextColor="#2d2d30" } catch (error) {
text60R console.error(error);
marginT-15 }
marginL-30 }}
/> />
<View style={styles.divider} marginT-8 />
<View row spread marginB-10 marginL-30 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191" />
<Text text70 marginL-10>
All day
</Text>
</View>
<View right marginR-30>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={event.allDay}
onValueChange={(value) =>
setEvent((prev) => ({ ...prev, allDay: value }))
}
/>
</View>
</View>
<View marginL-30 centerV>
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={event.start}
text70
marginL-8
maximumDate={event.end}
onChange={(date) => {
setEvent((prev) => ({ ...prev, start: date }));
}}
/>
</View>
<DateTimePicker
text70
value={event.start}
onChange={(date) => {
setEvent((prev) => ({ ...prev, start: date }));
}}
maximumDate={event.end}
dateTimeFormatter={(date, mode) => date.toLocaleTimeString("en-us",
{ hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>
</View>
{!event.allDay && (
<View row marginB-10 spread>
<View row centerV>
<Feather name="calendar" size={25} color="#919191" />
<DateTimePicker
value={event.end}
minimumDate={event.start}
text70
marginL-8
onChange={(date) => {
setEvent((prev) => ({ ...prev, end: date }));
}}
/>
</View> </View>
<DateTimePicker
text70
value={event.end}
minimumDate={event.start}
onChange={(date) => {
setEvent((prev) => ({ ...prev, end: date }));
}}
dateTimeFormatter={(date, mode) => date.toLocaleTimeString("en-us",
{ hour: "numeric",
minute: "numeric"
})}
mode="time"
marginR-30
/>
</View>
)}
</View>
<View style={styles.divider} />
<View marginH-30 marginB-10 row centerV> <TextField
<Ionicons name="person-circle-outline" size={28} color="#919191" /> placeholder="Edit event title"
<Text text70R marginL-10> value={event.title}
Assignees onChangeText={(text) => {
</Text> setEvent((prevEvent) => ({
<Button ...prevEvent!,
size={ButtonSize.small} title: text,
paddingH-8 }));
iconSource={() => ( }}
<Ionicons name="add-outline" size={20} color="#ea156c" /> placeholderTextColor="#2d2d30"
)} text60R
style={{ marginT-15
marginLeft: "auto", marginL-30
borderRadius: 8, />
backgroundColor: "#ffe8f1", <View style={styles.divider} marginT-8/>
borderColor: "#ea156c",
borderWidth: 1, <View row spread marginB-10 marginL-30 centerV>
}} <View row>
color="#ea156c" <AntDesign name="clockcircleo" size={24} color="#919191"/>
label="Assign" <Text text70 marginL-10>
/> All day
</View> </Text>
<View row marginH-13 marginT-13> </View>
<View <View right marginR-30>
marginL-30 <Switch
style={{ onColor={"#ea156c"}
aspectRatio: 1, offColor={"#e1e1e2"}
width: 50, marginL-10
backgroundColor: "red", value={event.allDay}
borderRadius: 50, onValueChange={(value) =>
}} setEvent((prev) => ({...prev!, allDay: value}))
/> }
<View />
marginL-30 </View>
style={{ </View>
aspectRatio: 1, <View marginL-30 centerV>
width: 50, <View row marginB-10 spread>
backgroundColor: "red", <View row centerV>
borderRadius: 50, <Feather name="calendar" size={25} color="#919191"/>
}} <DateTimePicker
/> value={event.start}
</View> text70
<View style={styles.divider} /> marginL-8
<View marginH-30 marginB-0 row spread centerV> maximumDate={event.end}
<View row centerV> onChange={(date) => {
<ClockIcon /> setEvent((prev) => ({...prev!, start: date}));
<Text text70 marginL-10> }}
Reminder />
</Text> </View>
</View> <DateTimePicker
<View> text70
<Button value={event.start}
size={ButtonSize.small} onChange={(date) => {
paddingH-8 setEvent((prev) => ({...prev!, start: date}));
iconSource={() => ( }}
<Ionicons name="add-outline" size={20} color="#ea156c" /> maximumDate={event.end}
)} dateTimeFormatter={(date) => date.toLocaleTimeString("en-us",
style={{ {
marginLeft: "auto", hour: "numeric",
borderRadius: 8, minute: "numeric"
backgroundColor: "#ffe8f1", })}
borderColor: "#ea156c", mode="time"
borderWidth: 1, marginR-30
}} />
color="#ea156c" </View>
label="Set Reminder"
/> {!event.allDay && (
</View> <View row marginB-10 spread>
</View> <View row centerV>
<View style={styles.divider} /> <Feather name="calendar" size={25} color="#919191"/>
<View marginH-30 marginB-0 row spread centerV> <DateTimePicker
<View row> value={event.end}
<LockIcon /> minimumDate={event.start}
<Text text70 marginL-10> text70
Mark as Private marginL-8
</Text> onChange={(date) => {
</View> setEvent((prev) => ({...prev!, end: date}));
<View> }}
<Switch />
onColor={"#ea156c"} </View>
offColor={"#e1e1e2"} <DateTimePicker
marginL-10 text70
value={event.private} value={event.end}
onValueChange={(value) => minimumDate={event.start}
setEvent((prev) => ({ ...prev, private: value })) onChange={(date) => {
} setEvent((prev) => ({...prev!, end: date}));
/> }}
</View> dateTimeFormatter={(date) => date.toLocaleTimeString("en-us",
</View> {
<View style={styles.divider} /> hour: "numeric",
<View marginH-30 marginB-0 row spread centerV> minute: "numeric"
<View row centerV> })}
<MenuIcon /> mode="time"
<Text text70 marginL-10> marginR-30
Add Details />
</Text> </View>
</View> )}
<View></View> </View>
</View> <View style={styles.divider}/>
</Dialog>
); <View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191"/>
<Text text70R marginL-10>
Assignees
</Text>
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c"/>
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
color="#ea156c"
label="Assign"
/>
</View>
<View row marginH-13 marginT-13>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
<View
marginL-30
style={{
aspectRatio: 1,
width: 50,
backgroundColor: "red",
borderRadius: 50,
}}
/>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon/>
<Text text70 marginL-10>
Reminder
</Text>
</View>
<View>
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c"/>
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
color="#ea156c"
label="Set Reminder"
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon/>
<Text text70 marginL-10>
Mark as Private
</Text>
</View>
<View>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={event.private}
onValueChange={(value) =>
setEvent((prev) => ({...prev!, private: value}))
}
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon/>
<Text text70 marginL-10>
Add Details
</Text>
</View>
<View></View>
</View>
</Dialog>
);
}; };
export default EditEventDialog; export default EditEventDialog;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 }, divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
gradient: { gradient: {
height: "25%", height: "25%",
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
width: "100%", width: "100%",
}, },
buttonContainer: { buttonContainer: {
position: "absolute", position: "absolute",
bottom: 25, bottom: 25,
width: "100%", width: "100%",
}, },
button: { button: {
backgroundColor: "rgb(253, 23, 117)", backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20, paddingVertical: 20,
}, },
topBtn: { topBtn: {
backgroundColor: "white", backgroundColor: "white",
color: "#05a8b6", color: "#05a8b6",
}, },
rotateSwitch: { rotateSwitch: {
marginLeft: 35, marginLeft: 35,
marginBottom: 10, marginBottom: 10,
marginTop: 25, marginTop: 25,
}, },
}); });

View File

@ -0,0 +1,63 @@
import React, {memo} from 'react';
import {Calendar} from "react-native-big-calendar";
import {StyleSheet} from "react-native";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useAtomValue, useSetAtom} from "jotai";
import {
editVisibleAtom,
eventForEditAtom,
modeAtom,
selectedDateAtom,
selectedNewEventDateAtom
} from "@/components/pages/calendar/atoms";
interface EventCalendarProps {
calendarHeight: number;
}
export const EventCalendar: React.FC<EventCalendarProps> = memo(({calendarHeight}) => {
const {data: events} = useGetEvents();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom)
const mode = useAtomValue(modeAtom)
const setEditVisible = useSetAtom(editVisibleAtom)
const setEventForEdit = useSetAtom(eventForEditAtom)
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom)
return (
<Calendar
bodyContainerStyle={styles.calHeader}
mode={mode}
events={events ?? []}
eventCellStyle={(event) => ({backgroundColor: event.eventColor})}
onPressEvent={(event) => {
setEditVisible(true);
setEventForEdit(event);
}}
height={calendarHeight}
activeDate={selectedDate}
date={selectedDate}
onPressCell={setSelectedNewEndDate}
headerContentStyle={mode === "day" ? styles.dayModeHeader : {}}
onSwipeEnd={(date) => {
setSelectedDate(date);
}}
/>
);
});
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
calHeader: {
borderWidth: 0,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
},
});

View File

@ -0,0 +1,41 @@
import {View} from "react-native-ui-lib";
import React, {useRef, useState} from "react";
import {LayoutChangeEvent} from "react-native";
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
import EditEventDialog from "@/components/pages/calendar/EditEventDialog";
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 calendarContainerRef = useRef(null);
const onLayout = (event: LayoutChangeEvent) => {
const {height} = event.nativeEvent.layout;
setCalendarHeight(height);
};
return (
<>
<View
style={{flex: 1, backgroundColor: "#fff", borderRadius: 30}}
ref={calendarContainerRef}
onLayout={onLayout}
>
<CalendarHeader/>
{calendarHeight > 0 && (
<EventCalendar
calendarHeight={calendarHeight}
/>
)}
</View>
<CalendarViewSwitch/>
<AddEventDialog/>
<EditEventDialog/>
<ManuallyAddEventModal/>
</>
)
}

View File

@ -1,490 +1,482 @@
import { import {
Avatar, Button,
Button, ButtonSize,
ButtonSize, Colors,
Colors, DateTimePicker,
DateTimePicker, LoaderScreen,
LoaderScreen, Modal,
Modal, Switch,
Picker, Text,
Switch, TextField,
Text, TouchableOpacity,
TextField, View,
TouchableOpacity,
View,
} from "react-native-ui-lib"; } from "react-native-ui-lib";
import { ScrollView } from "react-native-gesture-handler"; import {ScrollView} from "react-native-gesture-handler";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import {useSafeAreaInsets} from "react-native-safe-area-context";
import { useState } from "react"; import {useState} from "react";
import { import {AntDesign, Feather, Ionicons,} from "@expo/vector-icons";
AntDesign, import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types";
Feather, import {useCreateEvent} from "@/hooks/firebase/useCreateEvent";
Ionicons, import {EventData} from "@/hooks/firebase/types/eventData";
MaterialIcons, import {addHours} from "date-fns";
} from "@expo/vector-icons";
import { PickerMultiValue } from "react-native-ui-lib/src/components/picker/types";
import { useAuthContext } from "@/contexts/AuthContext";
import { useCreateEvent } from "@/hooks/firebase/useCreateEvent";
import { EventData } from "@/hooks/firebase/types/eventData";
import { addHours, setDate } from "date-fns";
import DropModalIcon from "@/assets/svgs/DropModalIcon"; import DropModalIcon from "@/assets/svgs/DropModalIcon";
import { CalendarEvent, useCalendarContext } from "@/contexts/CalendarContext"; import {StyleSheet} from "react-native";
import { repeatOptions } from "@/contexts/ToDosContext";
import { ImageBackground, StyleSheet } from "react-native";
import ClockIcon from "@/assets/svgs/ClockIcon"; import ClockIcon from "@/assets/svgs/ClockIcon";
import LockIcon from "@/assets/svgs/LockIcon"; import LockIcon from "@/assets/svgs/LockIcon";
import MenuIcon from "@/assets/svgs/MenuIcon"; import MenuIcon from "@/assets/svgs/MenuIcon";
import CameraIcon from "@/assets/svgs/CameraIcon"; import CameraIcon from "@/assets/svgs/CameraIcon";
import AssigneesDisplay from "@/components/shared/AssigneesDisplay"; import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
import {useAtom} from "jotai";
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
const daysOfWeek = [ const daysOfWeek = [
{ label: "Monday", value: "monday" }, {label: "Monday", value: "monday"},
{ label: "Tuesday", value: "tuesday" }, {label: "Tuesday", value: "tuesday"},
{ label: "Wednesday", value: "wednesday" }, {label: "Wednesday", value: "wednesday"},
{ label: "Thursday", value: "thursday" }, {label: "Thursday", value: "thursday"},
{ label: "Friday", value: "friday" }, {label: "Friday", value: "friday"},
{ label: "Saturday", value: "saturday" }, {label: "Saturday", value: "saturday"},
{ label: "Sunday", value: "sunday" }, {label: "Sunday", value: "sunday"},
]; ];
export const ManuallyAddEventModal = ({ export const ManuallyAddEventModal = () => {
show, const insets = useSafeAreaInsets();
close,
initialDate,
}: {
show: boolean;
close: () => void;
initialDate?: Date;
}) => {
const { addEvent } = useCalendarContext();
const { user } = useAuthContext();
const insets = useSafeAreaInsets();
const [title, setTitle] = useState<string>(""); const [selectedNewEventDate, setSelectedNewEndDate] = useAtom(selectedNewEventDateAtom)
const [isAllDay, setIsAllDay] = useState(false); const {show, close, initialDate} = {
const [isPrivate, setIsPrivate] = useState<boolean>(false); show: !!selectedNewEventDate,
const [startTime, setStartTime] = useState(() => { close: () => setSelectedNewEndDate(undefined),
const date = initialDate ?? new Date(); initialDate: selectedNewEventDate
date.setSeconds(0, 0);
return date;
});
const [endTime, setEndTime] = useState(() => {
const date = initialDate
? addHours(initialDate, 1)
: addHours(new Date(), 1);
date.setSeconds(0, 0);
return date;
});
const [startDate, setStartDate] = useState(initialDate ?? new Date());
const [endDate, setEndDate] = useState(initialDate ?? new Date());
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
const { mutateAsync: createEvent, isLoading, isError } = useCreateEvent();
const formatDateTime = (date?: Date | string) => {
if (!date) return undefined;
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
};
const combineDateAndTime = (date: Date, time: Date): Date => {
const combined = new Date(date);
combined.setHours(time.getHours());
combined.setMinutes(time.getMinutes());
combined.setSeconds(0);
combined.setMilliseconds(0);
return combined;
};
const handleSave = async () => {
let finalStartDate: Date;
let finalEndDate: Date;
if (isAllDay) {
finalStartDate = new Date(startDate.setHours(0, 0, 0, 0));
finalEndDate = new Date(startDate.setHours(0, 0, 0, 0));
} else {
finalStartDate = combineDateAndTime(startDate, startTime);
finalEndDate = combineDateAndTime(endDate, endTime);
} }
const eventData: Partial<EventData> = { const [title, setTitle] = useState<string>("");
title: title,
startDate: finalStartDate, const [isAllDay, setIsAllDay] = useState(false);
endDate: finalEndDate, const [isPrivate, setIsPrivate] = useState<boolean>(false);
allDay: isAllDay, const [startTime, setStartTime] = useState(() => {
const date = initialDate ?? new Date();
date.setSeconds(0, 0);
return date;
});
const [endTime, setEndTime] = useState(() => {
const date = initialDate
? addHours(initialDate, 1)
: addHours(new Date(), 1);
date.setSeconds(0, 0);
return date;
});
const [startDate, setStartDate] = useState(initialDate ?? new Date());
const [endDate, setEndDate] = useState(initialDate ?? new Date());
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
const {mutateAsync: createEvent, isLoading, isError} = useCreateEvent();
if (!selectedNewEventDate) return null;
const formatDateTime = (date?: Date | string) => {
if (!date) return undefined;
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
}; };
await createEvent(eventData); const combineDateAndTime = (date: Date, time: Date): Date => {
const combined = new Date(date);
combined.setHours(time.getHours());
combined.setMinutes(time.getMinutes());
combined.setSeconds(0);
combined.setMilliseconds(0);
return combined;
};
close(); const handleSave = async () => {
}; let finalStartDate: Date;
let finalEndDate: Date;
const getRepeatLabel = () => { if (isAllDay) {
const selectedDays = repeatInterval; finalStartDate = new Date(startDate.setHours(0, 0, 0, 0));
const allDays = [ finalEndDate = new Date(startDate.setHours(0, 0, 0, 0));
"sunday", } else {
"monday", finalStartDate = combineDateAndTime(startDate, startTime);
"tuesday", finalEndDate = combineDateAndTime(endDate, endTime);
"wednesday", }
"thursday",
"friday",
"saturday",
];
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
const isEveryWorkDay = workDays.every((day) => selectedDays.includes(day)); const eventData: Partial<EventData> = {
title: title,
startDate: finalStartDate,
endDate: finalEndDate,
allDay: isAllDay,
};
const isEveryDay = allDays.every((day) => selectedDays.includes(day)); await createEvent(eventData);
if (isEveryDay) { close();
return "Every day"; };
} else if (
isEveryWorkDay && const getRepeatLabel = () => {
!selectedDays.includes("saturday") && const selectedDays = repeatInterval;
!selectedDays.includes("sunday") const allDays = [
) { "sunday",
return "Every work day"; "monday",
} else { "tuesday",
return selectedDays "wednesday",
.map((item) => daysOfWeek.find((day) => day.value === item)?.label) "thursday",
.join(", "); "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
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<LoaderScreen message={"Saving event..."} color={Colors.grey40}/>
</Modal>
);
} }
};
if (isLoading && !isError) {
return ( return (
<Modal <Modal
visible={show} visible={show}
animationType="slide" animationType="slide"
onRequestClose={close} onRequestClose={close}
transparent={false} transparent={false}
>
<LoaderScreen message={"Saving event..."} color={Colors.grey40} />
</Modal>
);
}
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<View
style={{
flex: 1,
backgroundColor: "#fff",
paddingTop: insets.top, // Safe area inset for top
paddingBottom: insets.bottom, // Safe area inset for bottom
paddingLeft: insets.left, // Safe area inset for left
paddingRight: insets.right, // Safe area inset for right
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
padding: 16,
}}
> >
<TouchableOpacity onPress={close}> <View
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Cancel
</Text>
</TouchableOpacity>
<DropModalIcon onPress={close} />
<TouchableOpacity onPress={handleSave}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Save
</Text>
</TouchableOpacity>
</View>
<ScrollView>
<TextField
placeholder="Add event title"
value={title}
autoFocus
onChangeText={(text) => {
setTitle(text);
}}
placeholderTextColor="#2d2d30"
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);
}}
maximumDate={endDate}
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-8
/>
</View>
<DateTimePicker
value={startTime}
onChange={(date) => setStartTime(date)}
maximumDate={endTime}
minuteInterval={5}
dateTimeFormatter={(date, mode) =>
date.toLocaleTimeString("en-us", {
hour: "numeric",
minute: "numeric",
})
}
mode="time"
style={{ style={{
fontFamily: "PlusJakartaSans_500Medium", flex: 1,
fontSize: 16, backgroundColor: "#fff",
paddingTop: insets.top, // Safe area inset for top
paddingBottom: insets.bottom, // Safe area inset for bottom
paddingLeft: insets.left, // Safe area inset for left
paddingRight: insets.right, // Safe area inset for right
}} }}
marginR-30 >
/> <View
</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={{ style={{
fontFamily: "PlusJakartaSans_500Medium", flexDirection: "row",
fontSize: 16, justifyContent: "space-between",
padding: 16,
}} }}
/> >
<TouchableOpacity onPress={close}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Cancel
</Text>
</TouchableOpacity>
<DropModalIcon onPress={close}/>
<TouchableOpacity onPress={handleSave}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Save
</Text>
</TouchableOpacity>
</View> </View>
<DateTimePicker <ScrollView>
value={endTime} <TextField
onChange={(date) => setEndTime(date)} placeholder="Add event title"
minimumDate={startTime} value={title}
minuteInterval={5} autoFocus
dateTimeFormatter={(date, mode) => onChangeText={(text) => {
date.toLocaleTimeString("en-us", { setTitle(text);
hour: "numeric", }}
minute: "numeric", placeholderTextColor="#2d2d30"
}) style={{fontFamily: "Manrope_500Medium", fontSize: 22}}
} paddingT-15
mode="time" paddingL-30
style={{ returnKeyType="next"
fontFamily: "PlusJakartaSans_500Medium", />
fontSize: 16, <View style={styles.divider} marginT-8/>
}} <View marginL-30 centerV>
marginR-30 <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);
}}
maximumDate={endDate}
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-8
/>
</View>
<DateTimePicker
value={startTime}
onChange={(date) => setStartTime(date)}
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={(date) => setEndTime(date)}
minimumDate={startTime}
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 marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191"/>
<Text
style={{fontFamily: "Manrope_600SemiBold", fontSize: 18}}
marginL-10
>
Attendees
</Text>
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c"/>
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
color="#ea156c"
label="Add"
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
/>
</View>
<View marginL-35>
<AssigneesDisplay/>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon/>
<Text
style={{
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
}}
marginL-10
>
Reminders
</Text>
</View>
<View>
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c"/>
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
color="#ea156c"
label="Set Reminder"
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Mark as Private
</Text>
</View>
<View>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isPrivate}
onValueChange={(value) => setIsPrivate(value)}
/>
</View>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Add Details
</Text>
</View>
<View></View>
</View>
</ScrollView>
<Button
marginH-30
marginB-15
label="Create event from image"
text70
style={{height: 47}}
labelStyle={{fontFamily: "PlusJakartaSans_500Medium", fontSize: 15}}
backgroundColor="#05a8b6"
iconSource={() => (
<View marginR-5>
<CameraIcon color="white"/>
</View>
)}
/> />
</View>
)}
</View>
<View style={styles.divider} />
<View marginH-30 marginB-10 row centerV>
<Ionicons name="person-circle-outline" size={28} color="#919191" />
<Text
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 18 }}
marginL-10
>
Attendees
</Text>
<Button
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c" />
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
color="#ea156c"
label="Add"
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
/>
</View>
<View marginL-35>
<AssigneesDisplay />
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon />
<Text
style={{
fontFamily: "Manrope_600SemiBold",
fontSize: 18,
}}
marginL-10
>
Reminders
</Text>
</View> </View>
<View> </Modal>
<Button );
size={ButtonSize.small}
paddingH-8
iconSource={() => (
<Ionicons name="add-outline" size={20} color="#ea156c" />
)}
style={{
marginLeft: "auto",
borderRadius: 8,
backgroundColor: "#ffe8f1",
borderColor: "#ea156c",
borderWidth: 1,
}}
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
color="#ea156c"
label="Set Reminder"
/>
</View>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row>
<LockIcon />
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Mark as Private
</Text>
</View>
<View>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isPrivate}
onValueChange={(value) => setIsPrivate(value)}
/>
</View>
</View>
<View style={styles.divider} />
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<MenuIcon />
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Add Details
</Text>
</View>
<View></View>
</View>
</ScrollView>
<Button
marginH-30
marginB-15
label="Create event from image"
text70
style={{ height: 47 }}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 15 }}
backgroundColor="#05a8b6"
iconSource={() => (
<View marginR-5>
<CameraIcon color="white" />
</View>
)}
/>
</View>
</Modal>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 }, divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
gradient: { gradient: {
height: "25%", height: "25%",
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
width: "100%", width: "100%",
}, },
buttonContainer: { buttonContainer: {
position: "absolute", position: "absolute",
bottom: 25, bottom: 25,
width: "100%", width: "100%",
}, },
button: { button: {
backgroundColor: "rgb(253, 23, 117)", backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20, paddingVertical: 20,
}, },
topBtn: { topBtn: {
backgroundColor: "white", backgroundColor: "white",
color: "#05a8b6", color: "#05a8b6",
}, },
rotateSwitch: { rotateSwitch: {
marginLeft: 35, marginLeft: 35,
marginBottom: 10, marginBottom: 10,
marginTop: 25, marginTop: 25,
}, },
}); });

View File

@ -0,0 +1,9 @@
import { atom } from 'jotai';
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
export const editVisibleAtom = atom<boolean>(false);
export const eventForEditAtom = atom<CalendarEvent | undefined>(undefined);
export const isFamilyViewAtom = atom<boolean>(false);
export const modeAtom = atom<"week" | "month" | "day">("week");
export const selectedDateAtom = atom<Date>(new Date());
export const selectedNewEventDateAtom = atom<Date | undefined>(undefined);

View File

@ -0,0 +1,20 @@
export const modeMap = new Map([
[0, "day"],
[1, "week"],
[2, "month"],
]);
export const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];

View File

@ -0,0 +1,13 @@
export interface CalendarEvent {
id?: number | string; // Unique identifier for the event
user?: string;
title: string; // Event title or name
description?: string; // Optional description for the event
start: Date; // Start date and time of the event
end: Date; // End date and time of the event
location?: string; // Optional event location
allDay?: boolean; // Specifies if the event lasts all day
eventColor?: string; // Optional color to represent the event
participants?: string[]; // Optional list of participants or attendees
private?: boolean;
}

View File

@ -1,17 +1,17 @@
import {Button, ButtonSize, Dialog, Text, TextField, View} from "react-native-ui-lib"; import {Button, ButtonSize, Dialog, Text, TextField, View} from "react-native-ui-lib";
import React, {useEffect, useState} from "react"; import React, {useState} from "react";
import {useSignIn} from "@/hooks/firebase/useSignIn"; import {useSignIn} from "@/hooks/firebase/useSignIn";
import {StyleSheet} from "react-native"; import {StyleSheet} from "react-native";
import Toast from 'react-native-toast-message'; import Toast from 'react-native-toast-message';
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode"; import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
import {Camera, CameraView} from 'expo-camera'; import {Camera, CameraView} from 'expo-camera';
import {BarCodeScanner} from "expo-barcode-scanner";
const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> }) => { const SignInPage = ({setTab}: {
setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">>
}) => {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const [hasPermission, setHasPermission] = useState<boolean | null>(null); const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [scanned, setScanned] = useState<boolean>(false);
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false); const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
const {mutateAsync: signIn, error, isError} = useSignIn(); const {mutateAsync: signIn, error, isError} = useSignIn();
@ -19,7 +19,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
const handleSignIn = async () => { const handleSignIn = async () => {
await signIn({email, password}); await signIn({email, password});
if(!isError) { if (!isError) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Login successful!" text1: "Login successful!"
@ -33,10 +33,10 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
} }
}; };
const handleQrCodeScanned = async ({ data }: { data: string }) => { const handleQrCodeScanned = async ({data}: { data: string }) => {
setShowCameraDialog(false); setShowCameraDialog(false);
try { try {
await signInWithQrCode({ userId: data }); await signInWithQrCode({userId: data});
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Login successful with QR code!" text1: "Login successful with QR code!"
@ -51,9 +51,9 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
}; };
const getCameraPermissions = async (callback: () => void) => { const getCameraPermissions = async (callback: () => void) => {
const { status } = await Camera.requestCameraPermissionsAsync(); const {status} = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted'); setHasPermission(status === 'granted');
if(status === 'granted') { if (status === 'granted') {
callback(); callback();
} }
}; };
@ -83,12 +83,12 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
label="Login with a QR Code" label="Login with a QR Code"
onPress={() => { onPress={() => {
getCameraPermissions(() => setShowCameraDialog(true)); getCameraPermissions(() => setShowCameraDialog(true));
} }
} }
style={{marginBottom: 20}} style={{marginBottom: 20}}
backgroundColor="#fd1775" backgroundColor="#fd1775"
/> />
{isError && <Text center style={{marginBottom: 20}}>{`${error}`}</Text>} {isError && <Text center style={{marginBottom: 20}}>{`${error?.toString()?.split("]")?.[1]}`}</Text>}
<View row centerH marginB-5 gap-5> <View row centerH marginB-5 gap-5>
<Text text70> <Text text70>
@ -131,7 +131,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
bottom bottom
width="100%" width="100%"
height="70%" height="70%"
containerStyle={{ padding: 0 }} containerStyle={{padding: 0}}
> >
{hasPermission === null ? ( {hasPermission === null ? (
<Text>Requesting camera permissions...</Text> <Text>Requesting camera permissions...</Text>
@ -139,7 +139,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
<Text>No access to camera</Text> <Text>No access to camera</Text>
) : ( ) : (
<CameraView <CameraView
style={{ flex: 1 }} style={{flex: 1}}
onBarcodeScanned={handleQrCodeScanned} onBarcodeScanned={handleQrCodeScanned}
barcodeScannerSettings={{ barcodeScannerSettings={{
barcodeTypes: ["qr"], barcodeTypes: ["qr"],
@ -150,7 +150,7 @@ const SignInPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"re
label="Cancel" label="Cancel"
onPress={() => setShowCameraDialog(false)} onPress={() => setShowCameraDialog(false)}
backgroundColor="#fd1775" backgroundColor="#fd1775"
style={{ margin: 10 }} style={{margin: 10}}
/> />
</Dialog> </Dialog>
</View> </View>

View File

@ -1,118 +1,118 @@
import { View, Text, Button } from "react-native-ui-lib"; import {Button, Text, View} from "react-native-ui-lib";
import React, { useState } from "react"; import React, {useState} from "react";
import { StyleSheet } from "react-native"; import {StyleSheet} from "react-native";
import { Entypo, Ionicons, Octicons } from "@expo/vector-icons"; import {Octicons} from "@expo/vector-icons";
import CalendarSettingsPage from "./CalendarSettingsPage"; import CalendarSettingsPage from "./CalendarSettingsPage";
import ChoreRewardSettings from "./ChoreRewardSettings"; import ChoreRewardSettings from "./ChoreRewardSettings";
import UserSettings from "./UserSettings"; import UserSettings from "./UserSettings";
import { AuthContextProvider } from "@/contexts/AuthContext";
import ProfileIcon from "@/assets/svgs/ProfileIcon"; import ProfileIcon from "@/assets/svgs/ProfileIcon";
import CalendarIcon from "@/assets/svgs/CalendarIcon"; import CalendarIcon from "@/assets/svgs/CalendarIcon";
import PrivacyPolicyIcon from "@/assets/svgs/PrivacyPolicyIcon"; import PrivacyPolicyIcon from "@/assets/svgs/PrivacyPolicyIcon";
import ArrowRightIcon from "@/assets/svgs/ArrowRightIcon"; import ArrowRightIcon from "@/assets/svgs/ArrowRightIcon";
const pageIndex = { const pageIndex = {
main: 0, main: 0,
user: 1, user: 1,
calendar: 2, calendar: 2,
chore: 3, chore: 3,
policy: 4, policy: 4,
}; };
const SettingsPage = () => { const SettingsPage = () => {
const [selectedPage, setSelectedPage] = useState<number>(0); const [selectedPage, setSelectedPage] = useState<number>(0);
return ( return (
<View flexG> <View flexG>
{selectedPage == 0 && ( {selectedPage == 0 && (
<View flexG centerH marginH-30 marginT-30> <View flexG centerH marginH-30 marginT-30>
<Button <Button
backgroundColor="white" backgroundColor="white"
style={styles.mainBtn} style={styles.mainBtn}
children={ children={
<View row centerV width={"100%"}> <View row centerV width={"100%"}>
<ProfileIcon style={{ marginRight: 10 }} color="#07b9c8" /> <ProfileIcon style={{marginRight: 10}} color="#07b9c8"/>
<Text style={styles.label} color="#07b8c7"> <Text style={styles.label} color="#07b8c7">
Manage My Profile Manage My Profile
</Text> </Text>
<ArrowRightIcon style={{ marginLeft: "auto" }} /> <ArrowRightIcon style={{marginLeft: "auto"}}/>
</View> </View>
} }
onPress={() => setSelectedPage(pageIndex.user)} onPress={() => setSelectedPage(pageIndex.user)}
/> />
<Button <Button
backgroundColor="white" backgroundColor="white"
style={styles.mainBtn} style={styles.mainBtn}
children={ children={
<View row centerV width={"100%"}> <View row centerV width={"100%"}>
<CalendarIcon style={{ marginRight: 10 }} /> <CalendarIcon style={{marginRight: 10}}/>
<Text style={styles.label} color="#fd1775"> <Text style={styles.label} color="#fd1775">
Calendar Settings Calendar Settings
</Text> </Text>
<ArrowRightIcon style={{ marginLeft: "auto" }} /> <ArrowRightIcon style={{marginLeft: "auto"}}/>
</View> </View>
} }
onPress={() => { onPress={() => {
setSelectedPage(pageIndex.calendar); setSelectedPage(pageIndex.calendar);
}} }}
/> />
<Button <Button
backgroundColor="white" backgroundColor="white"
style={styles.mainBtn} style={styles.mainBtn}
children={ children={
<View row centerV width={"100%"}> <View row centerV width={"100%"}>
<Octicons <Octicons
name="gear" name="gear"
size={24} size={24}
color="#ff9900" color="#ff9900"
style={{ marginRight: 10 }} style={{marginRight: 10}}
/> />
<Text style={styles.label} color="#ff9900"> <Text style={styles.label} color="#ff9900">
To-Do Reward Settings To-Do Reward Settings
</Text> </Text>
<ArrowRightIcon style={{ marginLeft: "auto" }} /> <ArrowRightIcon style={{marginLeft: "auto"}}/>
</View> </View>
} }
onPress={() => setSelectedPage(pageIndex.chore)} onPress={() => setSelectedPage(pageIndex.chore)}
/> />
<Button <Button
backgroundColor="white" backgroundColor="white"
style={styles.mainBtn} style={styles.mainBtn}
children={ children={
<View row centerV width={"100%"}> <View row centerV width={"100%"}>
<PrivacyPolicyIcon style={{ marginRight: 10 }} /> <PrivacyPolicyIcon style={{marginRight: 10}}/>
<Text style={styles.label} color="#6c645b"> <Text style={styles.label} color="#6c645b">
Cally Privacy Policy Cally Privacy Policy
</Text> </Text>
<ArrowRightIcon style={{ marginLeft: "auto" }} /> <ArrowRightIcon style={{marginLeft: "auto"}}/>
</View> </View>
} }
/> />
</View>
)}
{selectedPage == pageIndex.calendar && (
<CalendarSettingsPage setSelectedPage={setSelectedPage}/>
)}
{selectedPage == pageIndex.chore && (
<ChoreRewardSettings setSelectedPage={setSelectedPage}/>
)}
{selectedPage == pageIndex.user && (
<UserSettings setSelectedPage={setSelectedPage}/>
)}
</View> </View>
)} );
{selectedPage == pageIndex.calendar && (
<CalendarSettingsPage setSelectedPage={setSelectedPage} />
)}
{selectedPage == pageIndex.chore && (
<ChoreRewardSettings setSelectedPage={setSelectedPage} />
)}
{selectedPage == pageIndex.user && (
<UserSettings setSelectedPage={setSelectedPage} />
)}
</View>
);
}; };
export default SettingsPage; export default SettingsPage;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
mainBtn: { mainBtn: {
width: 311, width: 311,
justifyContent: "flex-start", justifyContent: "flex-start",
marginBottom: 20, marginBottom: 20,
height: 57.61, height: 57.61,
}, },
label: { label: {
fontFamily: "Poppins_400Regular", fontFamily: "Poppins_400Regular",
fontSize: 14.71, fontSize: 14.71,
textAlignVertical: "center", textAlignVertical: "center",
}, },
}); });

View File

@ -99,6 +99,7 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => { const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
setUser(authUser); setUser(authUser);
if (authUser) { if (authUser) {
await refreshProfileData(authUser); await refreshProfileData(authUser);
const pushToken = await registerForPushNotificationsAsync(); const pushToken = await registerForPushNotificationsAsync();

View File

@ -1,194 +0,0 @@
// CalendarContext.tsx
import React, { createContext, useContext, useState, ReactNode } from "react";
// Define the CalendarEvent interface
export interface CalendarEvent {
id?: number | string; // Unique identifier for the event
user?: string;
title: string; // Event title or name
description?: string; // Optional description for the event
start: Date; // Start date and time of the event
end: Date; // End date and time of the event
location?: string; // Optional event location
allDay?: boolean; // Specifies if the event lasts all day
color?: string; // Optional color to represent the event
participants?: string[]; // Optional list of participants or attendees
private?: boolean;
}
// Define the context type
interface CalendarContextType {
events: CalendarEvent[];
familyEvents: CalendarEvent[];
addEvent: (event: CalendarEvent) => void; // Function to add an event
removeEvent: (id: number) => void; // Function to remove an event by ID
updateEvent: (changes: Partial<CalendarEvent>, id?: number) => void;
}
// Create the CalendarContext
const CalendarContext = createContext<CalendarContextType | undefined>(
undefined
);
// Create a provider component
export const CalendarProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [events, setEvents] = useState<CalendarEvent[]>([
{
id: 1,
title: "Team Meeting",
description: "Discuss project milestones and deadlines.",
start: new Date("2024-09-15T10:00:00"),
end: new Date("2024-09-15T11:00:00"),
location: "Office Conference Room",
allDay: false,
color: "#FF5733",
participants: ["Alice", "Bob", "Charlie"],
},
{
id: 2,
title: "Doctor's Appointment",
description: "Annual check-up with Dr. Smith.",
start: new Date("2024-09-20T14:30:00"),
end: new Date("2024-09-20T15:30:00"),
location: "Health Clinic",
allDay: false,
color: "#33FF57",
participants: ["You"],
},
{
id: 3,
title: "Birthday Party",
description: "Celebrating Sarah's 30th birthday.",
start: new Date("2024-09-25T18:00:00"),
end: new Date("2024-09-25T21:00:00"),
location: "Sarah's House",
allDay: false,
color: "#3357FF",
participants: ["You", "Sarah", "Tom", "Lily"],
},
{
id: 4,
title: "Project Deadline",
description: "Final submission for the project.",
start: new Date("2024-10-01T00:00:00"),
end: new Date("2024-10-01T23:59:00"),
location: "Online",
allDay: false,
color: "#FF33A1",
participants: ["You"],
},
{
id: 5,
title: "Halloween Costume Party",
description: "Join us for a spooky night of fun!",
start: new Date("2024-10-31T19:00:00"),
end: new Date("2024-10-31T23:00:00"),
location: "Downtown Club",
allDay: false,
color: "#FFB733",
participants: ["You", "Friends"],
},
]);
const [familyEvents, setFamilyEvents] = useState<CalendarEvent[]>([
{
id: 1,
user: "jakesId",
title: "Team Meeting",
description: "Discuss project milestones and deadlines.",
start: new Date("2024-09-10T10:00:00"),
end: new Date("2024-09-10T11:00:00"),
location: "Office Conference Room",
allDay: false,
color: "#FF5733",
participants: ["Alice", "Bob", "Charlie"],
},
{
id: 2,
user: "mikesId",
title: "Doctor's Appointment",
description: "Annual check-up with Dr. Smith.",
start: new Date("2024-09-21T14:30:00"),
end: new Date("2024-09-21T15:30:00"),
location: "Health Clinic",
allDay: false,
color: "#33FF57",
participants: ["You"],
},
{
id: 3,
user: "jakesId",
title: "Birthday Party",
description: "Celebrating Sarah's 30th birthday.",
start: new Date("2024-09-5T18:00:00"),
end: new Date("2024-09-5T21:00:00"),
location: "Sarah's House",
allDay: false,
color: "#3357FF",
participants: ["You", "Sarah", "Tom", "Lily"],
},
{
id: 4,
user: "davidsId",
title: "Project Deadline",
description: "Final submission for the project.",
start: new Date("2024-10-03T00:00:00"),
end: new Date("2024-10-03T23:59:00"),
location: "Online",
allDay: false,
color: "#FF33A1",
participants: ["You"],
},
{
id: 5,
user: "jakesId",
title: "Halloween Costume Party",
description: "Join us for a spooky night of fun!",
start: new Date("2024-10-02T19:00:00"),
end: new Date("2024-10-02T23:00:00"),
location: "Downtown Club",
allDay: false,
color: "#FFB733",
participants: ["You", "Friends"],
},
]);
// Function to add an event
const addEvent = (event: CalendarEvent) => {
event.id = events.length + 1;
setEvents((prevEvents) => [...prevEvents, event]);
};
// Function to remove an event by ID
const removeEvent = (id: number) => {
setEvents((prevEvents) => prevEvents.filter((event) => event.id !== id));
};
// Function to update an event
const updateEvent = ( changes: Partial<CalendarEvent>, id?: number) => {
setEvents((prevEvents) =>
prevEvents.map((event) =>
event.id === id ? { ...event, ...changes } : event
)
);
};
return (
<CalendarContext.Provider
value={{ events, addEvent, removeEvent, updateEvent, familyEvents }}
>
{children}
</CalendarContext.Provider>
);
};
// Custom hook to use the CalendarContext
export const useCalendarContext = () => {
const context = useContext(CalendarContext);
if (!context) {
throw new Error("useCalendar must be used within a CalendarProvider");
}
return context;
};

View File

@ -10,5 +10,5 @@ export interface EventData {
surpriseEvent?: boolean, surpriseEvent?: boolean,
notes?: string, notes?: string,
reminders?: string[] reminders?: string[]
id?: string, id?: string | number,
} }

View File

@ -2,9 +2,12 @@ import {useQuery} from "react-query";
import firestore from "@react-native-firebase/firestore"; import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext"; import {useAuthContext} from "@/contexts/AuthContext";
import {colorMap} from "@/contexts/SettingsContext"; import {colorMap} from "@/contexts/SettingsContext";
import {useAtomValue} from "jotai";
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
export const useGetEvents = (isFamilyView: boolean) => { export const useGetEvents = () => {
const { user, profileData } = useAuthContext(); const { user, profileData } = useAuthContext();
const isFamilyView = useAtomValue(isFamilyViewAtom)
return useQuery({ return useQuery({
queryKey: ["events", user?.uid, isFamilyView], queryKey: ["events", user?.uid, isFamilyView],

View File

@ -1,10 +1,8 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query"; import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore"; import firestore from "@react-native-firebase/firestore";
import {EventData} from "@/hooks/firebase/types/eventData"; import {EventData} from "@/hooks/firebase/types/eventData";
export const useUpdateEvent = () => { export const useUpdateEvent = () => {
const {user: currentUser} = useAuthContext()
const queryClients = useQueryClient() const queryClients = useQueryClient()
return useMutation({ return useMutation({
@ -13,7 +11,7 @@ export const useUpdateEvent = () => {
try { try {
await firestore() await firestore()
.collection("Events") .collection("Events")
.doc(eventData.id) .doc(`${eventData.id}`)
.update(eventData); .update(eventData);
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@ -441,7 +441,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "CallyFamilyPlanner"; PRODUCT_NAME = CallyFamilyPlanner;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -472,7 +472,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app; PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
PRODUCT_NAME = "CallyFamilyPlanner"; PRODUCT_NAME = CallyFamilyPlanner;
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";