diff --git a/package.json b/package.json index ed5e627..037ffb7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@howljs/calendar-kit": "^2.2.1", "@mobile-reality/react-native-select-pro": "^2.3.0", "@prisma/client": "^5.22.0", - "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-community/datetimepicker": "8.2.0", "@react-native-picker/picker": "2.9.0", "@react-navigation/material-top-tabs": "^6.6.14", @@ -54,6 +54,7 @@ "expo-haptics": "~14.0.1", "expo-image": "~2.0.6", "expo-linking": "~7.0.5", + "expo-radio-button": "^1.0.8", "expo-router": "~4.0.17", "expo-splash-screen": "~0.29.22", "expo-status-bar": "~2.0.1", diff --git a/src/app/(root)/(tabs)/(index)/Schedule.tsx b/src/app/(root)/(tabs)/(index)/Schedule.tsx index e70c490..802c3c3 100644 --- a/src/app/(root)/(tabs)/(index)/Schedule.tsx +++ b/src/app/(root)/(tabs)/(index)/Schedule.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { useCallback, useEffect, useState, useRef } from 'react'; +import { useCallback, useEffect, useState, useRef, useMemo } from 'react'; import { View, Text, @@ -8,10 +8,8 @@ import { StyleSheet, TextInput, Alert, - // this is a wrapper around the modal, will ensure that modal is closed when the external area has been pressed TouchableWithoutFeedback, Platform, - Button, NativeSyntheticEvent, ActivityIndicator, } from 'react-native'; @@ -19,12 +17,9 @@ import { CalendarBody, CalendarContainer, CalendarHeader, - DraggingEvent, - DraggingEventProps, PackedEvent, LocaleConfigsProps, CalendarKitHandle, - EventItem, } from '@howljs/calendar-kit'; import { Ionicons, AntDesign } from '@expo/vector-icons'; import { Ionicon } from '@/components/core/icon'; @@ -33,10 +28,13 @@ import { Dropdown } from 'react-native-element-dropdown'; import CalendarModeSwitcher, { CustomRecurrenceModal, } from '@/components/core/calendarModeSwitcher'; +import CalendarNavigation from '@/components/core/calendarNavigation'; +import { useLocalSearchParams } from 'expo-router'; +import DeleteSingleEvent, { DeleteRecurringEvents } from '@/components/core/deleteModals'; // TODO : define the edit event and new event modal as seperate components and pass down data as a prop instead // TODO : add an interface referencing the event useState hook -interface CalendarEvent { +export interface CalendarEvent { id: string; title: string; description?: string; @@ -45,38 +43,23 @@ interface CalendarEvent { color: string; location: string; isRecurring: boolean; - recurrence_frequency: any | string | undefined; // not entirely sure of the type + recurrence_frequency: any | string | undefined; isRecurringInstance?: boolean; parentEventId?: string | any; } -// experiment with the extension logic alongside a seperate independent interface to see which raises errors interface ExistingEventModal { - // input data for the current event related information the modal should render current_event: any; - - // useState variables that determines whether modal should be displayed or not + setEventListUseStateSetter: any; visibillity_state: boolean; delete_event_modal: boolean; - // this is based on the docs, added any just in case the previous two types fail to work - // this prop is intended to handle what will happen when modal is selected to be closed - onRequestClose: ((event: NativeSyntheticEvent) => void) | undefined | any; - - // this prop should be a useState hook that will handle whether the text input is edtiable - // ideally the text input should be editable when the edit button has been selected + delete_event_modal_recurring: boolean; isEditable: boolean; - - // deletes the current event upon selecting the delete icon - // TODO : determine and filter out the event based on the id - // the parameter that needs to be passed in is the current_event prop + onRequestClose: ((event: NativeSyntheticEvent) => void) | undefined | any; onRequestDelete: ((event: NativeSyntheticEvent) => void) | undefined | any; - start_time: any; end_time: any; - // This function will handle how the modal's data will be edited - // when the edit icon is selected onRequestEdit: ((event: NativeSyntheticEvent) => void) | undefined | any; - handleOnChangeTitle: any; handleOnChangeDescription: any; handleOnChangeStart: any; @@ -85,7 +68,7 @@ interface ExistingEventModal { dropdown_list: any; handleDropdownFunction: any; renderDropdownItem: any; - + radioButtonChangeHandler: (() => void) | any; handleChangeEventColor: ((event: NativeSyntheticEvent) => void) | undefined | any; handleSaveEditedEvent: ((event: NativeSyntheticEvent) => void) | undefined | any; handleCancelEditedEvent: any; @@ -97,11 +80,21 @@ interface ExistingEventModal { | ((event: NativeSyntheticEvent) => void) | undefined | any; + handleCloseRecurringDeleteModal: + | ((event: NativeSyntheticEvent) => void) + | undefined + | any; + listOfEvents: any[]; + currentSelectedRadioButton: string; + setListOfEventsSecondChild: any; + set_delete_event_modal_recurring: any; } -// TODO : Integrate logic for event deletion of existng event. +// TODO : requires lots of refactoring, badly written composition code +// TODO : see how you can replace prop drilling with context since there are quite a few repetitive props const ExistingEventModal = ({ current_event, + setEventListUseStateSetter, visibillity_state, onRequestClose, isEditable, @@ -120,11 +113,16 @@ const ExistingEventModal = ({ handleCancelEditedEvent, start_time, end_time, - - // additional props to handle the confirmation/deletion of a particular event handleOnPressDeleteConfirmation, handleOnPressDeleteCancellation, delete_event_modal, + delete_event_modal_recurring, + set_delete_event_modal_recurring, + handleCloseRecurringDeleteModal, + listOfEvents, + setListOfEventsSecondChild, + radioButtonChangeHandler, + currentSelectedRadioButton, }: ExistingEventModal) => { return ( @@ -189,92 +182,44 @@ const ExistingEventModal = ({ position: 'absolute', right: 0, top: -2, - flexDirection: 'row', // so that 2 items can be placed side by side + flexDirection: 'row', }} > - {/** Change it such that instead of delete icon, there's instead edit icon available */} - + - - - - - Are You Sure You Want to Delete This Event? - - - - Yes - - - No - - - - - + {current_event.isRecurring ? ( + + ) : ( + + )} @@ -297,17 +242,14 @@ const ExistingEventModal = ({ @@ -405,13 +338,10 @@ const ExistingEventModal = ({ @@ -439,9 +369,6 @@ const ExistingEventModal = ({ )} @@ -495,7 +422,6 @@ const ExistingEventModal = ({ { backgroundColor: color }, current_event.color === color && eventColorStyling.selectedColor, ]} - // onFocus={} onPress={handleChangeEventColor} /> ))} @@ -529,6 +455,110 @@ const ExistingEventModal = ({ }; export default function Schedule() { + // function to improve json syntax highlighting for debugging purpose + // copied from stack overflow + // function not being used anymore + function _syntaxHighlight(json: any) { + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace( + /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, + function (match: any) { + let cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + } + ); + } + + // relevant functions to handle deletion of events + /** + * @listOfEvents : the array of objects containing information about the events themselves. + * @event : the event to be deleted, which will be passed in as a parameter + */ + + // useState hook to keep track of which radio button has been selected + const [currentRadioButton, setCurrentRadioButton] = useState('all-event'); + const [radioButtonSelected, setRadioButtonSelected] = useState(false); + + // // helper functions to handle different instances of events that needs to be deleted + const deleteAllEvents = async (originalEvent: any[], eventToDelete: any) => { + // const parentEventID = eventToDelete.id; + // NOTE the negation operator within the filter predicate + // TODO : the logic for this isn't entirely correct, needs to be fixed + if (eventToDelete.id.includes('recurring')) { + const parentEventID = eventToDelete.parentId; + return await originalEvent.filter((currentEvent) => !currentEvent.id.includes(parentEventID)); + } + + // otherwise, if it happens to be an original event itself rather than recurring one + return originalEvent.filter((currentEvent) => !currentEvent.id.includes(eventToDelete.id)); + }; + + const deleteCurrentEvent = async (listOfEvents: any[], eventToDelete: any) => { + return await listOfEvents.filter((event) => event.id !== eventToDelete.id); + }; + const deleteSubsequentEvents = async (listOfEvents: any[], event: any) => { + // check if current event happens to be the recurring event + // otherwise, it's an original event (in which case we can go ahead and delete all events) + if (event.id.includes('recurring')) { + const event_id_array = event.id.split('_'); + const recurrence_unit = parseInt(event_id_array[event_id_array.length - 1]); + return listOfEvents.filter( + (currentEvent) => + !( + currentEvent.id.includes(event.parentEventId) && + parseInt(currentEvent.id.split('_')[currentEvent.id.split('_').length - 1]) > + recurrence_unit + ) + ); + } else { + await deleteAllEvents(listOfEvents, event); + } + }; + // TODO : needs wrapped around a function + const handleRecurringEventDeletion = async () => { + switch (currentRadioButton) { + // this means user wants to delete all subsequent events + // invoke the function to delete all the events corresponding to the id + // all three function variation will + case 'all-event': + await deleteAllEvents(eventsList, selectedEvent); + break; + case 'subsequent': + await deleteSubsequentEvents(eventsList, selectedEvent); + break; + case 'current': + await deleteCurrentEvent(eventsList, selectedEvent); + break; + default: + break; + } + }; + + // reference to determine currently selected radio button + const handleRadioButtonOnChange = (value: string) => { + console.log(`Selected radio button : ${value}`); + setCurrentRadioButton(value); + }; + + // determines if current radio button has been selected + const radioButtonSelectorUpdate = () => { + setRadioButtonSelected(true); + }; + const route_params = useLocalSearchParams(); + + // extract email (for payload) + const { email } = route_params; // supporting variables for the calendar mode switcher // NOTE : useRef allows us to persist values between renders /** @@ -802,6 +832,7 @@ export default function Schedule() { // this hook will determine whether to show the current existing event in the form of a modal const [showExistingEventModal, setShowExistingEventModal] = useState(false); + const [deleteEventModal, setDeleteEventModal] = useState(false); const [isModalEditable, setIsModalEditable] = useState(false); // this will determine the event that has been currently selected @@ -818,6 +849,21 @@ export default function Schedule() { const [showCustomRecurrenceModal, setShowCustomRecurrenceModal] = useState(false); const [customSelectedDays, setCustomSelectedDays] = useState([]); // stores the custom days the event should be repeated + // useState hook variable for currentCalendarDate + const [currentCalendarDate, setCurrentCalendarDate] = useState(new Date().toISOString()); + + // modal to handle rendering of recurrence modal + const [recurrenceDeleteModal, setRecurrenceDeleteModal] = useState(false); + + const handleCalendarDateChange = useCallback((newDate: string | any) => { + setCurrentCalendarDate(newDate); + calendarRef.current?.goToDate({ + date: newDate, + animatedDate: true, + hourScroll: true, + }); + }, []); + // this is just an example of how to add hours to the current time // this variable is intended to be a reference, it is not being used const _four_hours_delay = new Date().getHours() + 4; @@ -927,50 +973,25 @@ export default function Schedule() { } const uniqueId = `event_${Date.now()}_${Math.floor(Math.random() * 1000)}`; - console.log(`generated unique id : ${uniqueId}`); - // replaced with string based value for easier identification - // const random_generated_id = Math.floor(Math.random() * 100 + 1); - // console.log(random_generated_id); - // check if the newly generated id happens to exist within the current event list // TODO : Handle issue regarding existence of potential duplicate generated events as well const id_existence = eventsList.findIndex( (current_event: any) => current_event.id === uniqueId ); - // console.log(`id_existence value : ${id_existence}`); - - // in the event that this conditional is true, that means the id doesn't exist - // that means we can attach the current event's data with the new id that has been found - // save it into the list (which will then be sent to the database based on the email of the user) - // treating this also as a form of base case - // if (id_existence === -1) { - // const updatedEvent = { - // ...currentEventData, - // id: uniqueId, - // }; const newEvent = { ...currentEventData, id: uniqueId, }; let eventsToAdd = []; - - // if user wants an event to be recurring based on a specific selected frequency if (newEvent.isRecurring && newEvent.recurrence_frequency) { eventsToAdd = calculateRecurringEvents(newEvent); } else { - // otherwise, simply add a single instance copy of the particular event eventsToAdd = [newEvent]; } - - // double spread opearator - // since eventsToAdd can be more than one depending on recurrence frequency setEventList([...eventsList, ...eventsToAdd]); setNewEventModal(false); - - // reset current state data so we don't have to worry about it later - // NOTE : this would replace the logic for the handleCreateNewEvent function setCurrentEventData({ id: -1, title: '', @@ -1021,27 +1042,29 @@ export default function Schedule() { ); }; - // useCallback hook is being used as a wrapper around this reference function - // simple logic for handling recurring ev + // set regardless const handlePressEvent = useCallback( (event: CalendarEvent | any) => { // NOTE : the properties isRecurringInstance and parentEventId comes from the calculateRecurringEvents function // refer to the defintition of calculateRecurringEvents definition for reference - if (event.isRecurringInstance && event.parentEventId) { - // parentEvent : simply the original event set to be recurring - const parentEvent = eventsList.find((e: CalendarEvent) => e.id === event.parentEventId); - - if (parentEvent) { - setSelectedEvent(parentEvent); - } else { - setSelectedEvent(event); // otherwise, the original event should be set to selected - } - } else { - // TODO : this seems somewhat repetitive, find a fix for this - setSelectedEvent(event); - } - // console.log(`Pressed event : ${JSON.stringify(event)}`); // TODO : delete this statement, this is just to check if the event update is working as intended - // setSelectedEvent(event); + // if (event.isRecurringInstance && event.parentEventId) { + // // parentEvent : simply the original event set to be recurring + + // + // // const parentEvent = eventsList.find((e: CalendarEvent) => e.id === event.parentEventId); + + // // if (parentEvent) { + // // setSelectedEvent(parentEvent); + // // } else { + // // setSelectedEvent(event); + // // } + + // } else { + // // TODO : this seems somewhat repetitive, find a fix for this + // setSelectedEvent(event); + // } + // console.log(`Selected Event : ${JSON.stringify(event)}`); + setSelectedEvent(event); setShowExistingEventModal(true); }, [eventsList] @@ -1049,32 +1072,18 @@ export default function Schedule() { // prototype of the data that needs to be sent out to the datbase const events_payload = { - email: 'user email goes here', - events_data: 'information regarding new events goes here', + email: email, + events_data: eventsList, }; - // useEffect hook to test if sample event is working as intended - // TODO : delete this useState hooks (and console.log statements within it) - // useEffect(() => { - // console.log(`List of available events : ${JSON.stringify(eventsList)}`); - // // console.log('Detected changes to start date : ', startDate.toISOString()); - // // console.log('Detected changes to end date : ', endDate.toISOString()); - - // // console.log(`current selected event : ${JSON.stringify(selectedEvent)}`); - // // console.log(`current status of event modal display : ${showExistingEventModal}`); - // // console.log(currentEventData); - // // console.log(`current start and end date : \n${startDate}\n ${endDate}`); - // }, [ - // eventsList, - // // startDate, - // // endDate, - // // selectedEvent, - // // showExistingEventModal - // ]); - - // useEffect hook to check if recurrence modal state is being updated + useEffect(() => { - console.log('showCustomRecurrencModal : ', showCustomRecurrenceModal); - }, [showCustomRecurrenceModal]); + console.log(`Selected Event : ${JSON.stringify(selectedEvent)}`); + }); + // useEffect(() => { + // console.log('Detected changes to event lists.'); + // console.log(eventsList); + // }, [eventsList]); + return ( - {/* - TODO : the view isn't entirely functional - *Calendar mode switcher is intended to be added at the top */} + + {/* * insert acitivity Indicator animation loading logic here * through conditional rendering @@ -1101,26 +1112,24 @@ export default function Schedule() { // TODO : convert this into tailwindcss based styling for uniformity // the css here ensures that the button is positioned within the bottom right portion of the screen style={{ - position: 'absolute', // ensures that the element is positioned exactly where it has been specified - bottom: 10, // ensures that elements are placed on the bottom right portion of the screen - right: 10, // ensures that elements are positioned at the right hand side of the screen - zIndex: 1000, // determines the ordering of the elements toward which they should appear - backgroundColor: '#3498db', // skyblue hexademical code - borderRadius: 20, // creates a circular outline of the background color + position: 'absolute', + bottom: 10, + right: 10, + zIndex: 1000, + backgroundColor: '#3498db', + borderRadius: 20, width: 40, height: 40, - justifyContent: 'center', // ensures that the background and the content is aligned within the center + justifyContent: 'center', alignItems: 'center', shadowColor: '#000', - - // responsible for setting the drop shadow offset (native to IOS) shadowOffset: { width: 0, height: 2, }, - shadowOpacity: 0.3, // determines the visibillity of the shadow to be rendered - shadowRadius: 3, // sets the drop shadow blur radius - elevation: 5, // sets the elevation of android + shadowOpacity: 0.3, + shadowRadius: 3, + elevation: 5, }} // TODO : this requires some additioanl modifications onPress={handleCreateNewEvent} @@ -1135,25 +1144,23 @@ export default function Schedule() { timeZone="America/New_York" minDate="2025-01-01" maxDate="2026-12-31" - initialDate={new Date().toISOString().split('T')[0]} - numberOfDays={numberOfDays} // changed from 3 (static value) - // scrollByDay={numberOfDays <= 4} // should only hold true for smaller values + // initialDate={new Date().toISOString().split('T')[0]} + initialDate={currentCalendarDate.split('T')[0]} + numberOfDays={numberOfDays} scrollByDay={true} events={eventsList} - overlapType="overlap" // events should overlap, similar to google calendar - rightEdgeSpacing={1} // supporting prop related to event overlappping - // defines the minimum start time difference in minutes for events to be considered overlapping + overlapType="overlap" + rightEdgeSpacing={1} minStartDifference={15} - // to prevent unneccessary re-renders - // the event handler function will be memoized using useCallback hook - // refer to the function definition onPressEvent={handlePressEvent} + ref={calendarRef} > {showExistingEventModal && ( - // TODO : fix the issue with text input not working and changing the current functions into reusable reference functions { - setDeleteEventModal(false); + selectedEvent.isRecurring + ? setRecurrenceDeleteModal(false) + : setDeleteEventModal(false); }} handleOnPressDeleteConfirmation={async () => { // logic for deleting a particular event @@ -1162,15 +1169,21 @@ export default function Schedule() { ); // TODO : delete later, this is to experiment to check if the current event has been deleted or not console.log(`The updated events are : ${updatedEvents}`); - // set the newly updated event + // close the relevant modals after update has taken place setEventList(updatedEvents); setDeleteEventModal(false); setShowExistingEventModal(false); // close the event }} + // handles visibillity of single event modal delete_event_modal={deleteEventModal} + // handles visibillity of multiple event modal + delete_event_modal_recurring={recurrenceDeleteModal} + // NOTE that theoretically I can just define a reference function that closes the modal as well within the parent component instead (and potentially wrap it around using useCallback to prevent unneccessary re-renders) + set_delete_event_modal_recurring={setRecurrenceDeleteModal} end_time={endDate} start_time={startDate} // pass in the start and end date for the date time picker current_event={selectedEvent} + setEventListUseStateSetter={setEventList} visibillity_state={showExistingEventModal} onRequestClose={() => setShowExistingEventModal(false)} isEditable={isModalEditable} @@ -1184,14 +1197,8 @@ export default function Schedule() { ...prev, title: newUserInputTitle, })); - - // this wouldn't work due to asynchronous nature of the code - // setSelectedEvent(currentEventData); } }} - // TODO : change to a reference function for reusabillity - // TODO : fix this, the incorrect state is being updated here - // change from setCurrentEventData -> setSelectedEvent(prev => ...prev, { title : newUserInputTitle}) instead handleOnChangeDescription={(newUserInputDescription: any) => { if (isModalEditable) { setSelectedEvent((prevData: CalendarEvent) => ({ @@ -1266,20 +1273,23 @@ export default function Schedule() { setIsModalEditable(false); setShowExistingEventModal(false); }} - onRequestDelete={() => { - setDeleteEventModal(true); - // correct logic below for deleting a particular event - // // remove the event within the list whose current id matches the id of the currently selected event - // // include all other events except the current event containing matching id - // const updatedEvents = await eventsList.filter( - // (event) => event.id !== selectedEvent.id - // ); - // // TODO : delete later, this is to experiment to check if the current event has been deleted or not - // console.log(`The updated events are : ${updatedEvents}`); - // // set the newly updated event - // setEventList(updatedEvents); - // setShowExistingEventModal(false); // close the event + // slight variation based on whether event is recurring or not + // due to the need for the modals that needs to be displayed being different from one another + onRequestDelete={async () => { + // need to conditionally handle which modal to set true + if (await selectedEvent.isRecurring) { + setRecurrenceDeleteModal(true); + } else { + setDeleteEventModal(true); + } }} + handleCloseRecurringDeleteModal={() => setRecurrenceDeleteModal(false)} + listOfEvents={eventsList} + setListOfEventsSecondChild={setEventList} + // props for radio button selection + currentSelectedRadioButton={currentRadioButton} + radioButtonChangeHandler={handleRadioButtonOnChange} + // radioButtonSelectorhandler={radioButtonSelectorUpdate as any} /> )} @@ -1831,3 +1841,24 @@ const ButtonStyling = StyleSheet.create({ fontSize: 16, }, }); + +const recurrenceEventStyling = StyleSheet.create({ + eventsOptionStyle: { + padding: 2, + borderRadius: 4, + width: '20%', + alignItems: 'center', + marginBottom: 3, + }, + textStyles: { + color: 'white', + fontWeight: 'bold', + fontSize: 16, + }, + modifiedButtonStyling: { + padding: 10, + borderRadius: 5, + width: '42%', + alignItems: 'center', + }, +}); diff --git a/src/app/onboarding3.tsx b/src/app/onboarding3.tsx index acc9ddb..96f1746 100644 --- a/src/app/onboarding3.tsx +++ b/src/app/onboarding3.tsx @@ -192,6 +192,8 @@ const OnboardingScreen3: React.FC = () => { // old navigation route router.push({ pathname: '/(root)/(tabs)/(index)/Schedule', + + // router parameter data is being passed here, retrieve it within Schedule.tsx file params: { email: payload.email }, }); // point the user to the correct path } else { diff --git a/src/components/core/calendarNavigation/index.tsx b/src/components/core/calendarNavigation/index.tsx new file mode 100644 index 0000000..15fb888 --- /dev/null +++ b/src/components/core/calendarNavigation/index.tsx @@ -0,0 +1,306 @@ +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Modal, + Platform, + TouchableWithoutFeedback, +} from 'react-native'; +import { useEffect, useState } from 'react'; +import DateTimePicker from '@react-native-community/datetimepicker'; +// component wrapper around Ionicons (refer to the definition of the component itself) +import { Ionicon } from '../icon'; +const CalendarNavigation = ({ currentDate, onDateChange }) => { + // useState hook for DateTimePicker component enhancement + // reused logic from start and end date time from Schedule component + // determines whether datetimepicker component should be displayed or not + const [showDatePickerModal, setShowDatePickerModal] = useState(false); + const [datePickerValue, setDatePickerValue] = useState(new Date()); + + // For IOS, conditionally render the datetimepicker + const [showIOSDatePicker, setShowIOSDatePicker] = useState(false); + const [centerString, setCenterString] = useState('Today'); + + // Function to handle date picker changes + const handleDatePickerChange = (event, selectedDate) => { + const currentDate = selectedDate || datePickerValue; + setDatePickerValue(currentDate); + }; + + // functon to confirm date selection + const confirmDateSelection = () => { + onDateChange(datePickerValue.toISOString()); + setShowDatePickerModal(false); + setCenterString('Jump To Today'); + }; + + const FormatDisplayDate = (date) => { + // @reference https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString + const options: any = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }; + return new Date(date).toLocaleDateString(undefined, options); + }; + + // 3 void functions to decrement current month, increment current month, and navigate to current date + // handle previous month navigation + // all the final dates needs to be converted to ISOString() using the built in method from Date + const goToPrevMonth = () => { + // retrieves the current date that has been selected + // backtrack by a single month + const date = new Date(currentDate); + date.setMonth(date.getMonth() - 1); + setCenterString('Jump To Today'); + onDateChange(date.toISOString()); // update currentDate + }; + + const goToNextMonth = () => { + const date = new Date(currentDate); + date.setMonth(date.getMonth() + 1); + setCenterString('Jump To Today'); + onDateChange(date.toISOString()); // update currentDate + }; + + const goToToday = () => { + onDateChange(new Date().toISOString()); + setCenterString('Today'); + }; + + // handle opening date picker + const openDatePicker = () => { + if (Platform.OS === 'ios') { + setShowIOSDatePicker(true); + setShowDatePickerModal(true); + } else { + // handle every other platform + setShowDatePickerModal(true); + } + }; + useEffect(() => { + console.log( + `current date prop value : ${FormatDisplayDate(currentDate)}, \n actual current date (should be today's date) : ${new Date().toDateString()} ` + ); + }, [currentDate]); + return ( + <> + + + + + + + {centerString} + + + + + + + {/* + * fine-tune the navigation logic + */} + setShowDatePickerModal(true)} + style={navigationStyles.dateTextContainer} + > + {FormatDisplayDate(currentDate)} + + + + + {/** Date time picker modal to navigate within different portion of the calendar */} + { + setShowDatePickerModal(false); + setShowIOSDatePicker(false); + }} + > + {/**TouchableWithoutFeedback has the opposite behavior of TouchableOpacity and generally useful when something needs to be clicked but doesn't require any kind of animation present */} + { + // not entirely sure why the state should be set to false here instead of true + setShowDatePickerModal(false); + setShowIOSDatePicker(false); + // setShowDatePickerModal(true) + // setShowIOSDatePicker(true) + }} + > + + + + Select a Date + {/**Conditionally render datetime pciker for IOS */} + {(Platform.OS === 'android' || (Platform.OS === 'ios' && showIOSDatePicker)) && ( + + )} + + {/**on IOS, show an explicit button to render the datepicker */} + {Platform.OS === 'ios' && !showIOSDatePicker && ( + + Show Date Picker + + )} + + { + setShowDatePickerModal(false); + setShowIOSDatePicker(false); + }} + > + Cancel + + + Save + + + + + + + + + ); +}; + +// styles for navigation component +const navigationStyles = StyleSheet.create({ + container: { + paddingVertical: 10, + paddingHorizontal: 15, + backgroundColor: '#fff', + flexDirection: 'column', + borderBottomWidth: 1, + borderBottomColor: '#f0f0f0', + }, + dateControls: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 5, + }, + navButton: { + padding: 5, + }, + todayButton: { + paddingHorizontal: 15, + paddingVertical: 5, + marginHorizontal: 10, + backgroundColor: '#e6f2ff', + borderRadius: 15, + }, + todayText: { + color: '#3498db', + fontWeight: 'bold', + }, + dateText: { + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + color: '#333', + }, + + // the styling here really doesn't make much of a difference to the text + // TODO : this styling might be unneccessary, so remove it + dateTextContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + calendarIcon: { + marginLeft: 8, + }, +}); + +// define styles for the DateTimePicker modal for fine-tuned navigation +const dateTimePickerStyles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + }, + + // reused modal style code (refer to Schedule.tsx) + modalView: { + width: '80%', // adjust as needed + backgroundColor: 'white', + borderRadius: 10, + padding: 20, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + title: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 15, + color: '#333', + }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + marginTop: 20, + }, + button: { + padding: 12, + borderRadius: 5, + width: '45%', + alignItems: 'center', + }, + buttonConfirm: { + backgroundColor: '#3498db', + }, + buttonCancel: { + backgroundColor: '#e74c3c', + }, + buttonText: { + color: 'white', + fontWeight: 'bold', + fontSize: 16, + }, + showPickerButton: { + backgroundColor: '#3498db', + padding: 10, + borderRadius: 5, + marginVertical: 15, + }, + showPickerText: { + color: 'white', + fontWeight: 'bold', + }, +}); + +export default CalendarNavigation; diff --git a/src/components/core/deleteModals/index.tsx b/src/components/core/deleteModals/index.tsx new file mode 100644 index 0000000..5002d0c --- /dev/null +++ b/src/components/core/deleteModals/index.tsx @@ -0,0 +1,635 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// requires 2 seperate imports +import { Modal, View, Text, TouchableOpacity } from 'react-native'; +import RadioButtonGroup, { RadioButtonItem } from 'expo-radio-button'; +import { CalendarEvent } from '@/app/(root)/(tabs)/(index)/Schedule'; +import VisibleDateProvider from '@howljs/calendar-kit/lib/typescript/context/VisibleDateProvider'; + +// interface for deleting single event +interface DeleteSingleEventInterface { + visibillity: boolean; + onPressDeleteConfirmation: any; + onPressDeleteCancellation: any; + buttonStyling: any; +} +// basic delete modal to handle deletion of single events (that isn't recurring) +const DeleteSingleEvent = ({ + visibillity, + onPressDeleteConfirmation, + onPressDeleteCancellation, + buttonStyling, +}: DeleteSingleEventInterface) => { + return ( + + + + + Are Your Sure you want to delete this Event? + + + + Yes + + + No + + + + + + ); +}; + +export default DeleteSingleEvent; + +interface DeleteRecurringEventsType { + visible: boolean; + setModalVisibillity: any; + setEventsLists: any; + onPressDeleteConfirmation?: any; + onPressDeleteCancellation?: any; + buttonStyling: any; + recurrenceEventStyles: any; + list_of_events: CalendarEvent[] | any[]; + handleOnRequestModalClose: any; + selectedEvent: CalendarEvent; + handleRadioButtonOnChange: any; + + // TODO : prop drilling within ExistingEventModal needed + handleRecurringEventDeletionCallback: any | undefined; + currentRadioButton: string; + setEventsList: any; +} + +// TODO : implement this +export const DeleteRecurringEvents = ({ + visible, + setModalVisibillity, + setEventsLists, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onPressDeleteConfirmation, + onPressDeleteCancellation, + buttonStyling, + recurrenceEventStyles, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + list_of_events, + handleOnRequestModalClose, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + selectedEvent, + handleRadioButtonOnChange, + currentRadioButton, + handleRecurringEventDeletionCallback, + setEventsList, +}: DeleteRecurringEventsType) => { + const deleteAllEvents = () => { + // TODO : the logic for this isn't entirely correct, needs to be fixed + if (selectedEvent.id.includes('recurring')) { + const parentEventID = selectedEvent.parentEventId; + return list_of_events.filter((currentEvent) => !currentEvent.id.includes(parentEventID)); + } + + return list_of_events.filter((currentEvent) => !currentEvent.id.includes(selectedEvent.id)); + }; + + const deleteCurrentEvent = async () => { + return await list_of_events.filter((event) => event.id !== selectedEvent.id); + }; + + const deleteSubsequentEvents = async () => { + console.log(`Selected Event : ${JSON.stringify(selectedEvent)}`); + if (selectedEvent.id.includes('recurring')) { + const event_id_array = selectedEvent.id.split('_'); + const recurrence_unit = parseInt(event_id_array[event_id_array.length - 1]); + const filtered_events = list_of_events.filter( + (currentEvent) => + !( + currentEvent.id.includes(selectedEvent.parentEventId) && + parseInt(currentEvent.id.split('_')[currentEvent.id.split('_').length - 1]) > + recurrence_unit + ) + ); + console.log(`Data from filtered events : ${JSON.stringify(filtered_events)}`); + return filtered_events; + } else { + return await deleteAllEvents(); + } + }; + + const handleRecurringEventDeletionInternal = async () => { + let updated_event_list: any = []; + try { + switch (currentRadioButton) { + case 'all-event': + updated_event_list = await deleteAllEvents(); + console.log('all-event case function has been trieggered!'); + break; + + case 'subsequent': + updated_event_list = await deleteSubsequentEvents(); + console.log('subsequent case function has been trieggered!'); + break; + + case 'current': + updated_event_list = await deleteCurrentEvent(); + console.log('current case function has been trieggered!'); + break; + default: + break; + } + + console.info(`Updated list of events : ${updated_event_list}`); + setEventsList(updated_event_list); + setModalVisibillity(!visible); + } catch (error) { + console.error('Error : ', error); + return error; + } + return updated_event_list; + }; + return ( + + + + + Delete This Recurring Events? + + + + + { + // setRadioButtonSelected(true); + // }} + // onSelected={handleOnSelectedRadioButtons} + label={This and the following events} + /> + { + // setRadioButtonSelected(true); + // }} + // onSelected={handleOnSelectedRadioButtons} + label={This Event Only} + /> + + + + Cancel + + { + // // const mutated_event = handleRecurringEventDeletionInternal(); + // // console.log('Retrieved mutated event is : ', JSON.stringify(mutated_event)); + // // setEventsList(mutated_event); + // // console.log(`Toggling modal visibillity within child component.`); + // // setModalVisibillity(!visible); + + // }} + onPress={handleRecurringEventDeletionInternal} + > + Ok + + + + + + + ); +}; + +// NOTE : sample typescript code placed here for reference, this should be transferred over to Schedule.tsx: + +const sample_id_original = 'event_1744902168035_736'; +const sample_id_recurring = 'event_1744902168035_736_recurring_1'; + +// test to check resulting output converting string into array +const sample_id_original_array = sample_id_original.split('_'); +const sample_id_original_recurring_array = sample_id_recurring.split('_'); + +// Output : ["event", "1744902168035", "736"] +// console.log(sample_id_original_array); + +// Output : ["event", "1744902168035", "736", "recurring", "1"] +// console.log(sample_id_original_recurring_array); + +// then we can simply check if the original ID contains the particular val +// console.log(sample_id_original.includes(sample_id_original_array[1] + "_" + sample_id_original_array[2])); // output : true +// console.log(sample_id_recurring.includes(sample_id_original_array[1] + "_" + sample_id_original_array[2])); // output : true +// console.log(`ID match? : ${sample_id_original === sample_id_original_array[0] + "_" + sample_id_original_array[1] + "_" + sample_id_original_array[2]}`); + +const sample_data = [ + { + // this object should not be deleted when deleteAllEvents is called (since this is a different original event) + id: 'event_1744902168038_736', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-18T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-18T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + }, + { + id: 'event_1744902168035_736', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-18T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-18T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + }, + { + id: 'event_1744902168035_736_recurring_1', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-19T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-19T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_2', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-20T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-20T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_8', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-26T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-26T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_9', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-27T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-27T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_15', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-05-03T13:01:00.000Z', + }, + end: { + dateTime: '2025-05-03T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_16', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-05-04T13:01:00.000Z', + }, + end: { + dateTime: '2025-05-04T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_22', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-05-10T13:01:00.000Z', + }, + end: { + dateTime: '2025-05-10T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_23', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-05-11T13:01:00.000Z', + }, + end: { + dateTime: '2025-05-11T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_29', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-05-17T13:01:00.000Z', + }, + end: { + dateTime: '2025-05-17T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_30', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-05-18T13:01:00.000Z', + }, + end: { + dateTime: '2025-05-18T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, + { + id: 'event_1744902168035_736_recurring_36', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-05-24T13:01:00.000Z', + }, + + end: { + dateTime: '2025-05-24T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', + }, +]; +// we want to remove all indicators of recurring events +// NOTE : this is experimental as the intention is to build out additional data based on this + +// another way of deleting this would be to first determine all id of the particular events that needs to be deleted (which will be stored as an array of strings) +// which will then be iterated over +// this function assumes that we only want the original event to be retained +const deleteRecurringEvents = (originalEventsData: any[]) => { + // try removing the original event and retian all the recurring ones for now + // suppose we only want the original id + + // this will ensure that the original event is retained and recurring events are deleted. + const filtered_data = originalEventsData.filter( + (currentEvent) => !currentEvent.id.includes(sample_id_original + '_recurring') + ); + console.log(JSON.stringify(filtered_data)); +}; + +// /** +// * @originalEventsData : the array of objects containing information about the events themselves. +// * @event : the event to be deleted, which will be passed in as a parameter +// */ +// const deleteSubsequentEvents = (listOfEvents: any[], event: any) => { +// // check if current event happens to be the recurring event +// // otherwise, it's an original event (in which case we can go ahead and delete all events) +// if (event.id.includes("recurring")) { +// const event_id_array = event.id.split("_"); +// const recurrence_unit = parseInt(event_id_array[event_id_array.length - 1]); +// return listOfEvents.filter((currentEvent) => !(currentEvent.id.includes(event.parentEventId) && parseInt(currentEvent.id.split('_')[-1]) > recurrence_unit)); +// } else { +// deleteAllEvents(listOfEvents, event); +// } +// }; + +// this function should execute if user wants to delete all instances of the original event and subsequent recurring events +// first we need the parent id corresponding to the event +const deleteAllEvents = (originalEvent: any[], eventToDelete: any) => { + // const parentEventID = eventToDelete.id; + // NOTE the negation operator within the filter predicate + return originalEvent.filter((currentEvent) => !currentEvent.id.includes(eventToDelete.id)); +}; + +const subsequent_event_to_delete = { + id: 'event_1744902168035_736_recurring_9', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-27T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-27T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', + isRecurringInstance: true, + parentEventId: 'event_1744902168035_736', +}; + +const sample_original_event = { + id: 'event_1744902168035_736', + title: 'Repeat During Wekeend', + description: 'This Event Will Repeat Every Weekend', + start: { + dateTime: '2025-04-18T13:01:00.000Z', + }, + end: { + dateTime: '2025-04-18T15:01:00.000Z', + }, + color: '#DB4437', + location: 'Not Specified', + isRecurring: true, + recurrence_frequency: 'Every Weekend', +}; +// deleteSubsequentEvents(sample_data, sample_original_event); diff --git a/yarn.lock b/yarn.lock index b4fdcb8..688bccc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1887,10 +1887,10 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.0" -"@react-native-async-storage/async-storage@1.23.1": - version "1.23.1" - resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz#cad3cd4fab7dacfe9838dce6ecb352f79150c883" - integrity sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA== +"@react-native-async-storage/async-storage@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz#8aae432adfc20800308e2ef3ce380864f0f9def8" + integrity sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow== dependencies: merge-options "^3.0.4" @@ -4788,6 +4788,11 @@ expo-modules-core@2.2.3: dependencies: invariant "^2.2.4" +expo-radio-button@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/expo-radio-button/-/expo-radio-button-1.0.8.tgz#5f5c411ae0a2da633ae4b56d87a59dfa23a47bae" + integrity sha512-sE+aCtq5/lL1Y24oVYMyGug0IPt0R3dXS6rSK2S+A+EFm6HYBj0ZKGA35ywSekS+/8lE9UwKS+ORwQlWsfXoUQ== + expo-router@~4.0.17: version "4.0.19" resolved "https://registry.yarnpkg.com/expo-router/-/expo-router-4.0.19.tgz#b2c33c07f18c81af0692ee06b6f1e3dee50da979"