Files
cally/components/pages/calendar/ManuallyAddEventModal.tsx
2025-02-14 15:05:42 +01:00

833 lines
34 KiB
TypeScript

import {
Button,
ButtonSize,
Colors,
DateTimePicker,
Dialog,
LoaderScreen,
Modal,
Picker,
PickerModes,
Switch,
Text,
TextField,
TextFieldRef,
TouchableOpacity,
View,
} from "react-native-ui-lib";
import {ScrollView} from "react-native-gesture-handler";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import {useEffect, useRef, useState} from "react";
import {AntDesign, Feather, Ionicons} from "@expo/vector-icons";
import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateEvent} from "@/hooks/firebase/useCreateEvent";
import {EventData} from "@/hooks/firebase/types/eventData";
import {Alert, StyleSheet} from "react-native";
import ClockIcon from "@/assets/svgs/ClockIcon";
import LockIcon from "@/assets/svgs/LockIcon";
import MenuIcon from "@/assets/svgs/MenuIcon";
import CameraIcon from "@/assets/svgs/CameraIcon";
import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
import {useAtom} from "jotai";
import {eventForEditAtom, isAllDayAtom, selectedNewEventDateAtom,} from "@/components/pages/calendar/atoms";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import DeleteEventDialog from "./DeleteEventDialog";
import {useDeleteEvent} from "@/hooks/firebase/useDeleteEvent";
import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
import {addHours, format, isAfter, isSameDay, startOfMinute} from "date-fns";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {Calendar} from "react-native-calendars";
import React from "react";
const daysOfWeek = [
{label: "Monday", value: "monday"},
{label: "Tuesday", value: "tuesday"},
{label: "Wednesday", value: "wednesday"},
{label: "Thursday", value: "thursday"},
{label: "Friday", value: "friday"},
{label: "Saturday", value: "saturday"},
{label: "Sunday", value: "sunday"},
];
export const ManuallyAddEventModal = () => {
const insets = useSafeAreaInsets();
const {user, profileData} = useAuthContext();
const [selectedNewEventDate, setSelectedNewEndDate] = useAtom(
selectedNewEventDateAtom
);
const [editEvent, setEditEvent] = useAtom(eventForEditAtom);
const [allDayAtom, setAllDayAtom] = useAtom(isAllDayAtom);
const [deleteModalVisible, setDeleteModalVisible] = useState<boolean>(false);
const {mutateAsync: deleteEvent, isPending: isDeleting} = useDeleteEvent();
const {show, close, initialDate} = {
show: !!selectedNewEventDate || !!editEvent,
close: () => {
setDeleteModalVisible(false);
setSelectedNewEndDate(undefined);
setEditEvent(undefined);
setCreator(null);
},
initialDate: selectedNewEventDate || editEvent?.start,
};
const detailsRef = useRef<TextFieldRef>(null);
const [title, setTitle] = useState<string>(editEvent?.title || "");
const [details, setDetails] = useState<string>(editEvent?.notes || "");
const [isAllDay, setIsAllDay] = useState(editEvent?.allDay || false);
const [isPrivate, setIsPrivate] = useState<boolean>(
editEvent?.private || false
);
const [location, setLocation] = useState(editEvent?.location ?? "");
const [showStartDatePicker, setShowStartDatePicker] = useState(false);
const [showEndDatePicker, setShowEndDatePicker] = useState(false);
useEffect(() => {
if (allDayAtom === true) setIsAllDay(true);
}, [allDayAtom]);
const [startTime, setStartTime] = useState(() => {
const date = initialDate ?? new Date();
if (
date.getMinutes() > 0 ||
date.getSeconds() > 0 ||
date.getMilliseconds() > 0
) {
date.setHours(date.getHours() + 1);
}
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
});
const [endTime, setEndTime] = useState(() => {
if (editEvent?.end) {
return new Date(editEvent.end);
}
const baseDate = editEvent?.end ?? initialDate ?? new Date();
return addHours(baseDate, 1);
});
const [startDate, setStartDate] = useState(initialDate ?? new Date());
const [endDate, setEndDate] = useState(
editEvent?.end ?? initialDate ?? new Date()
);
const [selectedAttendees, setSelectedAttendees] = useState<string[]>(
editEvent?.attendees ?? [user?.uid]
);
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
const {
mutateAsync: createEvent,
isPending: isAdding,
isError,
} = useCreateEvent();
const {data: members} = useGetFamilyMembers(true);
const titleRef = useRef<TextFieldRef>(null);
const [creator, setCreator] = useState("");
useEffect(() => {
if (editEvent) {
let creatorMember = members?.find(
(member) => member?.uid === editEvent.creatorId
);
const fullName = `${creatorMember?.firstName ?? ""}`;
setCreator(fullName);
}
}, [members]);
const isLoading = isDeleting || isAdding;
useEffect(() => {
setTitle(editEvent?.title || "");
setDetails(editEvent?.notes || "");
setIsAllDay(editEvent?.allDay || false);
setIsPrivate(editEvent?.private || false);
setStartTime(() => {
const date = initialDate ? new Date(initialDate) : new Date();
date.setSeconds(0, 0);
return date;
});
setEndTime(() => {
if (editEvent?.end) {
return startOfMinute(new Date(editEvent.end));
}
const baseDate = editEvent?.end ?? initialDate ?? new Date();
return addHours(baseDate, 1);
});
setStartDate(initialDate ?? new Date());
setEndDate(editEvent?.end ?? initialDate ?? new Date());
setSelectedAttendees(editEvent?.attendees ?? [user?.uid]);
setLocation(editEvent?.location ?? "");
setRepeatInterval([]);
}, [editEvent, selectedNewEventDate]);
useEffect(() => {
if (show && !editEvent) {
setTimeout(() => {
titleRef?.current?.focus();
}, 500);
}
}, [selectedNewEventDate]);
if (!show) return null;
const showDeleteEventModal = () => {
setDeleteModalVisible(true);
};
const handleDeleteEvent = async () => {
await deleteEvent({eventId: `${editEvent?.id}`});
close();
};
const hideDeleteEventModal = () => {
setDeleteModalVisible(false);
};
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> = {
title: title,
startDate: finalStartDate,
endDate: finalEndDate,
allDay: isAllDay,
attendees: selectedAttendees,
notes: details,
location: location,
private: isPrivate
};
if (editEvent?.id) eventData.id = editEvent?.id;
if (validateEvent()) {
await createEvent(eventData);
setEditEvent(undefined);
close();
} else {
return;
}
};
const validateEvent = () => {
if (!title) {
Alert.alert("Alert", "Title field cannot be empty");
return false;
}
if (!selectedAttendees || selectedAttendees?.length === 0) {
Alert.alert('Alert', 'Cannot have an event without any attendees');
return false;
}
return true;
};
if (isLoading && !isError) {
return (
<Modal
visible={show}
animationType="slide"
onRequestClose={close}
transparent={false}
>
<LoaderScreen
message={isDeleting ? "Deleting event..." : "Saving event..."}
color={Colors.grey40}
/>
</Modal>
);
}
const renderCalendarPicker = (
isStart: boolean,
visible: boolean,
onDismiss: () => void
) => {
const currentDate = isStart ? startDate : endDate;
const setDate = isStart ? setStartDate : setEndDate;
return (
<Dialog
visible={visible}
onDismiss={onDismiss}
panDirection={"down"}
width="100%"
bottom
containerStyle={{
backgroundColor: Colors.white,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
paddingBottom: insets.bottom,
}}
>
<View padding-20>
<View row spread marginB-20>
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
Select Date
</Text>
<TouchableOpacity onPress={onDismiss}>
<Text style={{color: Colors.$textPrimary}}>Done</Text>
</TouchableOpacity>
</View>
<Calendar
firstDay={1}
current={format(currentDate, 'yyyy-MM-dd')}
minDate={isStart ? undefined : format(startDate, 'yyyy-MM-dd')}
markedDates={{
[format(currentDate, 'yyyy-MM-dd')]: {
selected: true,
selectedColor: '#ea156c'
}
}}
onDayPress={(day) => {
const newDate = new Date(day.timestamp);
newDate.setHours(currentDate.getHours());
newDate.setMinutes(currentDate.getMinutes());
setDate(newDate);
if(isStart) {
if (startTime.getHours() > endTime.getHours() &&
(isSameDay(newDate, endDate) || isAfter(newDate, endDate))) {
const newEndDate = new Date(newDate);
newEndDate.setDate(newEndDate.getDate() + 1);
newEndDate.setHours(endDate.getHours());
newEndDate.setMinutes(endDate.getMinutes());
setEndDate(newEndDate);
}
} else {
if (endTime.getHours() < startTime.getHours() &&
(isSameDay(newDate, startDate) || isAfter(startDate, newDate))) {
const newStartDate = new Date(newDate);
newStartDate.setDate(newStartDate.getDate() - 1);
newStartDate.setHours(startDate.getHours());
newStartDate.setMinutes(startDate.getMinutes());
setStartDate(newStartDate);
}
}
onDismiss();
}}
theme={{
selectedDayBackgroundColor: '#ea156c',
selectedDayTextColor: '#ffffff',
todayTextColor: '#ea156c',
arrowColor: '#ea156c',
}}
enableSwipeMonths={true}
/>
</View>
</Dialog>
);
};
const renderDateSection = () => (
<View marginL-30 centerV>
<View row spread marginB-10 centerV>
<View row>
<AntDesign name="clockcircleo" size={24} color="#919191"/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
All day
</Text>
</View>
<View right marginR-30>
<Switch
onColor={"#ea156c"}
offColor={"#e1e1e2"}
marginL-10
value={isAllDay}
onValueChange={(value) => setIsAllDay(value)}
/>
</View>
</View>
<View row marginB-10 spread centerV>
<View row centerV>
<Feather name="calendar" size={25} color="#919191"/>
<TouchableOpacity onPress={() => setShowStartDatePicker(true)}>
<Text marginL-8 style={styles.dateText}>
{format(startDate, 'MMM d, yyyy')}
</Text>
</TouchableOpacity>
</View>
{!isAllDay && (
<View right marginR-30>
<DateTimePicker
value={startTime}
is24Hour={profileData?.userType === ProfileType.PARENT ? false : true}
onChange={(time) => {
if (endDate.getDate() === startDate.getDate() &&
time.getHours() >= endTime.getHours() && time.getMinutes() >= endTime.getHours())
{
const newEndDate = new Date(endDate);
setStartTime(time);
newEndDate.setDate(newEndDate.getDate() + 1);
setEndDate(newEndDate);
}
else setStartTime(time);
}}
minuteInterval={5}
mode="time"
timeFormat={profileData?.userType === ProfileType.PARENT ? "hh:mm A" : "HH:mm A"}
style={[styles.timePicker]}
/>
</View>
)}
</View>
{!isAllDay && (
<View row marginB-10 spread centerV>
<View row centerV>
<Feather name="calendar" size={25} color="#919191"/>
<TouchableOpacity onPress={() => setShowEndDatePicker(true)}>
<Text marginL-8 style={styles.dateText}>
{format(endDate, 'MMM d, yyyy')}
</Text>
</TouchableOpacity>
</View>
<View right marginR-30>
<DateTimePicker
value={endTime}
is24Hour={profileData?.userType === ProfileType.PARENT ? false : true}
onChange={(time) => {
setEndTime(time);
if (
endDate.getDate() === startDate.getDate() &&
time.getHours() <= startTime.getHours() &&
time.getMinutes() <= startTime.getMinutes()
) {
const newEndDate = new Date(endDate);
newEndDate.setDate(newEndDate.getDate() + 1);
setEndDate(newEndDate);
}
}}
minuteInterval={5}
mode="time"
timeFormat={profileData?.userType === ProfileType.PARENT ? "hh:mm A" : "HH:mm A"}
style={[styles.timePicker]}
/>
</View>
</View>
)}
{renderCalendarPicker(true, showStartDatePicker, () => setShowStartDatePicker(false))}
{renderCalendarPicker(false, showEndDatePicker, () => setShowEndDatePicker(false))}
</View>
)
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
}}
>
{/*{editEvent ? (*/}
{/* <>*/}
{/* <View center paddingT-8>*/}
{/* <TouchableOpacity onPress={close}>*/}
{/* <DropModalIcon />*/}
{/* </TouchableOpacity>*/}
{/* </View>*/}
{/* <View row spread paddingH-10 paddingB-15>*/}
{/* <Button*/}
{/* color="#05a8b6"*/}
{/* style={styles.topBtn}*/}
{/* iconSource={() => <CloseXIcon />}*/}
{/* onPress={close}*/}
{/* />*/}
{/* <View row>*/}
{/* <Button*/}
{/* style={styles.topBtn}*/}
{/* marginR-10*/}
{/* iconSource={() => <PenIcon />}*/}
{/* onPress={handleSave}*/}
{/* />*/}
{/* <Button*/}
{/* style={styles.topBtn}*/}
{/* marginL-5*/}
{/* iconSource={() => <BinIcon />}*/}
{/* onPress={() => {*/}
{/* showDeleteEventModal();*/}
{/* }}*/}
{/* />*/}
{/* </View>*/}
{/* </View>*/}
{/* </>*/}
{/*) : (*/}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
padding: 16,
}}
>
<TouchableOpacity onPress={close}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Cancel
</Text>
</TouchableOpacity>
<View flexS row gap-10>
<TouchableOpacity onPress={handleSave}>
<Text
style={{
color: "#05a8b6",
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
}}
text70
>
Save
</Text>
</TouchableOpacity>
</View>
</View>
{/*)}*/}
<ScrollView style={{minHeight: "81%"}}>
<TextField
placeholder="Add event title"
ref={titleRef}
value={title}
onChangeText={(text) => {
setTitle(text);
}}
placeholderTextColor="#2d2d30"
style={{fontFamily: "Manrope_500Medium", fontSize: 22}}
paddingT-15
paddingL-30
returnKeyType="next"
/>
<View style={styles.divider} marginT-8/>
{renderDateSection()}
<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: 16}}
marginL-10
>
Attendees
</Text>
<View flex-1/>
<Picker
value={selectedAttendees}
onChange={(value) =>
setSelectedAttendees((value as string[]) ?? [])
}
style={{marginLeft: "auto"}}
mode={PickerModes.MULTI}
renderInput={() => (
<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,
}}
/>
)}
>
{members?.map((member) => (
<Picker.Item
key={member?.uid}
value={member?.uid!}
label={member?.firstName + " " + member?.lastName}
/>
))}
</Picker>
</View>
<View marginL-35>
<AssigneesDisplay
setSelectedAttendees={setSelectedAttendees}
selectedAttendees={selectedAttendees}
/>
</View>
<View style={styles.divider}/>
<View marginH-30 marginB-0 row spread centerV>
<View row centerV>
<ClockIcon/>
<Text
style={{
fontFamily: "Manrope_600SemiBold",
fontSize: 16,
}}
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 center>
<LockIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-12
center
>
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-28 marginB-0 centerV flex-1>
<View row centerV style={{flexGrow: 1}}>
<Ionicons name="location-outline" size={25} color={"#919191"}/>
<TextField
placeholder="Location"
value={location}
onChangeText={(text) => {
setLocation(text);
}}
placeholderTextColor="#2d2d30"
style={{
fontFamily: "Manrope_500Medium",
fontSize: 16,
minWidth: "100%",
}}
marginL-12
paddingR-12
/>
</View>
</View>
{editEvent && (
<>
<View style={styles.divider}/>
<View marginH-32 marginB-0 centerV flex-1>
<View row centerV style={{flexGrow: 1}}>
<AddPersonIcon/>
<TextField
editable={false}
value={creator}
placeholderTextColor="#2d2d30"
style={{
fontFamily: "Manrope_500Medium",
fontSize: 16,
minWidth: "100%",
color: "black",
}}
marginL-12
paddingR-12
/>
</View>
</View>
</>
)}
<View style={styles.divider}/>
<View marginH-30 marginB-0 spread centerV flex-1>
<TouchableOpacity onPress={() => detailsRef?.current?.focus()}>
<View row centerV>
<MenuIcon/>
<Text
style={{
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
}}
marginL-10
>
Add Details
</Text>
</View>
</TouchableOpacity>
<TextField
value={details}
onChangeText={setDetails}
ref={detailsRef}
maxLength={2000}
multiline
numberOfLines={10}
marginT-10
style={{flex: 1, minHeight: 180}}
/>
</View>
</ScrollView>
<Button
disabled
marginH-30
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>
)}
/>
{editEvent && (
<TouchableOpacity
onPress={showDeleteEventModal}
style={{ marginTop: 15, marginBottom: 40, alignSelf: "center" }}
hitSlop={{left: 30, right: 30, top: 10, bottom: 10}}
>
<Text
style={{
color: "#ff1637",
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 15,
}}
>
Delete Event
</Text>
</TouchableOpacity>
)}
</View>
{editEvent && (
<DeleteEventDialog
visible={deleteModalVisible}
title={editEvent?.title}
onDismiss={hideDeleteEventModal}
onConfirm={handleDeleteEvent}
/>
)}
</Modal>
</>
);
};
const styles = StyleSheet.create({
divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
gradient: {
height: "25%",
position: "absolute",
bottom: 0,
width: "100%",
},
buttonContainer: {
position: "absolute",
bottom: 25,
width: "100%",
},
button: {
backgroundColor: "rgb(253, 23, 117)",
paddingVertical: 20,
},
topBtn: {
backgroundColor: "white",
color: "#05a8b6",
},
rotateSwitch: {
marginLeft: 35,
marginBottom: 10,
marginTop: 25,
},
timePicker: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
},
dateText: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16,
},
dateButton: {
marginTop: 10,
}
});