diff --git a/src/app/(root)/(tabs)/(index)/Schedule.tsx b/src/app/(root)/(tabs)/(index)/Schedule.tsx index 268fc25..e70c490 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 } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { View, Text, @@ -8,12 +8,12 @@ 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'; import { CalendarBody, @@ -23,14 +23,33 @@ import { DraggingEventProps, PackedEvent, LocaleConfigsProps, + CalendarKitHandle, + EventItem, } from '@howljs/calendar-kit'; import { Ionicons, AntDesign } from '@expo/vector-icons'; import { Ionicon } from '@/components/core/icon'; import DateTimePicker from '@react-native-community/datetimepicker'; import { Dropdown } from 'react-native-element-dropdown'; -import { ShowerHeadIcon } from 'lucide-react-native'; +import CalendarModeSwitcher, { + CustomRecurrenceModal, +} from '@/components/core/calendarModeSwitcher'; // 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 { + id: string; + title: string; + description?: string; + start: { dateTime: string }; + end: { dateTime: string }; + color: string; + location: string; + isRecurring: boolean; + recurrence_frequency: any | string | undefined; // not entirely sure of the type + 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 @@ -38,7 +57,7 @@ interface ExistingEventModal { // useState variables that determines whether modal should be displayed or not 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; @@ -53,7 +72,7 @@ interface ExistingEventModal { 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; @@ -61,17 +80,27 @@ interface ExistingEventModal { handleOnChangeTitle: any; handleOnChangeDescription: any; handleOnChangeStart: any; + handleOnChangeEnd: any; handleOnPressRecurring: any; dropdown_list: any; handleDropdownFunction: any; renderDropdownItem: any; - handleChangeEventColor: ((event: NativeSyntheticEvent) => void) | undefined | any; - handleSaveEditedEvent: ((event: NativeSyntheticEvent) => void) | undefined | any; + handleChangeEventColor: ((event: NativeSyntheticEvent) => void) | undefined | any; + handleSaveEditedEvent: ((event: NativeSyntheticEvent) => void) | undefined | any; handleCancelEditedEvent: any; + handleOnPressDeleteConfirmation: + | ((event: NativeSyntheticEvent) => void) + | undefined + | any; + handleOnPressDeleteCancellation: + | ((event: NativeSyntheticEvent) => void) + | undefined + | any; } + +// TODO : Integrate logic for event deletion of existng event. const ExistingEventModal = ({ - // TODO : define the relevant props needed to be rendered current_event, visibillity_state, onRequestClose, @@ -81,6 +110,7 @@ const ExistingEventModal = ({ handleOnChangeTitle, handleOnChangeDescription, handleOnChangeStart, + handleOnChangeEnd, handleOnPressRecurring, dropdown_list, handleDropdownFunction, @@ -89,6 +119,12 @@ const ExistingEventModal = ({ handleSaveEditedEvent, handleCancelEditedEvent, start_time, + end_time, + + // additional props to handle the confirmation/deletion of a particular event + handleOnPressDeleteConfirmation, + handleOnPressDeleteCancellation, + delete_event_modal, }: ExistingEventModal) => { return ( Event Details - {/* - * TODO : figure out how to place the two items side by side next to one another - - - - */} - {/** 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 + + + + + @@ -217,21 +302,14 @@ const ExistingEventModal = ({ borderRadius: 5, // determines the curvature of the edges around the squares padding: 10, // determines how much extra space should be added to push out the borders fontSize: 14, // determines how large the text should appear within the input box - backgroundColor: '#f9f9f9', // determines the color within the input box itself + + // gray out is isEditable is set to false + backgroundColor: isEditable ? '#ffffff' : '#f5f5f5', + color: isEditable ? '#000000' : '#888888', shadowOffset: { width: 10, height: 10 }, // shadowRadius: 20, }} value={current_event.title || ''} - // placeholder="Enter event title" // placeholder not needed atp - // NOTE : we only want to update the title portion of the currentEvent state - // all the other fields will remain unchanged - // onChangeText={(newUserInputTitle) => - // setCurrentEventData((prev) => ({ - // ...prev, - // title: newUserInputTitle, - // })) - // } - onChangeText={handleOnChangeTitle} /> @@ -257,7 +335,8 @@ const ExistingEventModal = ({ borderRadius: 5, padding: 10, fontSize: 12, - backgroundColor: '#f9f9f9', + backgroundColor: isEditable ? '#ffffff' : '#f5f5f5', + color: isEditable ? '#000000' : '#888888', height: 80, textAlignVertical: 'bottom', }} @@ -265,6 +344,7 @@ const ExistingEventModal = ({ // current_event prop should contain a property // named description + editable={isEditable} value={current_event.description} onChangeText={handleOnChangeDescription} multiline={true} @@ -293,7 +373,9 @@ const ExistingEventModal = ({ Start: + + + End: + + + this is helpful for persisitng data + * @useState : is to keep data between renders (updating does fire re-rendering) + * + * Additional information: + * @param {Callback} - a callback is a function passed as an argument to another function (this is the most basic definition) + * + * + */ + + const calendarRef = useRef(null); + + // by default, the mode should be set to 3 days + const [calendarMode, setCalendarMode] = useState('3day'); + const [numberOfDays, setNumberOfDays] = useState(3); + + // useState hook for AcitivityIndicator + // AcitivityIndicator is intended to display circular loading indicator + const [isLoading, setIsLoading] = useState(false); + + // function to handle different calendar modes + // the function should be wrapped around an useCallback hook + const handleModeChange = useCallback((mode: string, days: number) => { + setIsLoading(true); // start loading animation + + // update state values to adjust mode and days + setCalendarMode(mode); + setNumberOfDays(days); + + // Handle month view special case + if (mode === 'month') { + Alert.alert('Month View', 'Month View is not supported yet.'); + setCalendarMode('week'); + setNumberOfDays(7); + } + + // a js native api + // tells the browser that I wish to perform an animation + // accepts a callback function (aka the animation logic) + requestAnimationFrame(() => { + calendarRef.current?.goToDate({ + date: new Date().toISOString(), + animatedDate: true, + hourScroll: true, + }); + + // add a small delay to finish the transition before stopping the loading state + setTimeout(() => { + setIsLoading(false); + }, 600); + }); + }, []); + + // dropdown data for recurring events const dropdownData = [ { label: 'Daily', value: '1' }, { label: 'Weekly', value: '2' }, { label: 'Annually', value: '3' }, { label: 'Every Weekday', value: '4' }, { label: 'Every Weekend', value: '5' }, - - // TODO : this should be a feature post-mvp (as it would be more work to implement atm) - // { label : 'custom', value : '6' } + { label: 'Custom', value: '6' }, ]; + // function to calculate recurring events based on recurrence frequency + const calculateRecurringEvents = useCallback((event: any) => { + // handle the edge cases in which the event does not recur + // simply return an array containing a single element + // the element being the event itself + if (!event.isRecurring || !event.recurrence_frequency) { + return [event]; + } + + // otherwise, use spread operator to copy the original event + // and store it within originalEvent variable + const originalEvent = { ...event }; + + const additionalEvents = []; + const startDate = new Date(event.start.dateTime); + const endDate = new Date(event.end.dateTime); + const duration = endDate.getTime() - startDate.getTime(); + console.log( + `value of duration : ${duration}, value of start time : ${startDate}, value of end date : ${endDate}` + ); + + // switch statement to handle the cases for event recurrence + switch (event.recurrence_frequency) { + // since we want the date to repeat each day + // NOTE : i = 1 so that the same event doesn't repeat twice + case 'Daily': + for (let i = 1; i <= 120; i++) { + // calculate the start + const newStart = new Date(startDate); + newStart.setDate(startDate.getDate() + i); + + // calculate the end + const newEnd = new Date(newStart.getTime() + duration); + // const newEnd = new Date(endDate); + endDate.setDate(endDate.getDate() + duration); + + // add the events to the additional events array + // NOTE : observe the 2 new properties being added here [and the other switch statement cases] (isRecurringInstance, parentEventId) + additionalEvents.push({ + ...originalEvent, + id: `${originalEvent.id}_recurring_${i}`, + start: { dateTime: newStart.toISOString() }, + end: { dateTime: newEnd.toISOString() }, + isRecurringInstance: true, + parentEventId: originalEvent.id, + }); + } + break; + + // similar logic to daily + // primary differentiation is that i is being multiplied by 7 + // for the newStart date calculation + case 'Weekly': + for (let i = 1; i <= 112; i++) { + const newStart = new Date(startDate); + newStart.setDate(startDate.getDate() + i * 7); + const newEnd = new Date(newStart.getTime() + duration); + + additionalEvents.push({ + ...originalEvent, + id: `${originalEvent.id}_recurring_${i}`, + start: { dateTime: newStart.toISOString() }, + end: { dateTime: newEnd.toISOString() }, + isRecurringInstance: true, + parentEventId: originalEvent.id, + }); + } + break; + + case 'Every Weekday': + // last for 4 months (since each semester spans 4 month timeframe) + for (let i = 1; i <= 120; i++) { + const newStart = new Date(startDate); + newStart.setDate(startDate.getDate() + i); + if (newStart.getDay() === 0 || newStart.getDay() === 6) { + continue; + } + + const newEnd = new Date(newStart.getTime() + duration); + + additionalEvents.push({ + ...originalEvent, + id: `${originalEvent.id}_recurring_${i}`, + start: { dateTime: newStart.toISOString() }, + end: { dateTime: newEnd.toISOString() }, + isRecurringInstance: true, + parentEventId: originalEvent.id, + }); + } + break; + + case 'Every Weekend': + // let weekendCount = 0 + for (let i = 1; i <= 120; i++) { + const newStart = new Date(startDate); + newStart.setDate(startDate.getDate() + i); + + // avoid weekdays? + // not entirely sure of this logic + if (newStart.getDay() !== 0 && newStart.getDay() !== 6) { + continue; + } + + const newEnd = new Date(newStart.getTime() + duration); // observe the recurring logic + + additionalEvents.push({ + ...originalEvent, + id: `${originalEvent.id}_recurring_${i}`, + start: { dateTime: newStart.toISOString() }, + end: { dateTime: newEnd.toISOString() }, + isRecurringInstance: true, + parentEventId: originalEvent.id, + }); + } + break; + + case 'Annually': + for (let i = 1; i <= 5; i++) { + const newStart = new Date(startDate); + newStart.setFullYear(startDate.getFullYear() + i); + + console.log('new start value (annually) : ', JSON.stringify(newStart)); + const newEnd = new Date(newStart.getTime() + duration); + additionalEvents.push({ + ...originalEvent, + id: `${originalEvent.id}_recurring_${i}`, + start: { dateTime: newStart.toISOString() }, + end: { dateTime: newEnd.toISOString() }, + isRecurringInstance: true, + parentEventId: originalEvent.id, + }); + } + break; + + // add logic for rendering custom event modal + case 'Custom': + // get selected days from custom recurrence + // TODO : the original event may need customRecurrenceDays field added to it + // eslint-disable-next-line no-case-declarations + const customDays = event.customRecurrenceDays || []; + if (customDays.length === 0) break; // if no custom days has been provided, nothing to repeat + + // create events for next 52 weeks + // TODO : adjust this value accordingly + for (let i = 1; i <= 365; i++) { + const newStart = new Date(startDate); + newStart.setDate(startDate.getDate() + i); + + // only create events for selected days of the week + if (!customDays.includes(newStart.getDay())) { + continue; + } + + const newEnd = new Date(newStart.getTime() + duration); + additionalEvents.push({ + ...originalEvent, + id: `${originalEvent.id}_recurring_${i}`, + start: { dateTime: newStart.toISOString() }, + end: { dateTime: newEnd.toISOString() }, + isRecurringInstance: true, + parentEventId: originalEvent.id, + }); + } + break; + + default: + break; // do nothing + } + + return [originalEvent, ...additionalEvents]; + }, []); + // define the function to render events const renderEvent = useCallback( (event: PackedEvent) => ( @@ -441,15 +781,28 @@ export default function Schedule() { > {event.title} + {event.isRecurringInstance && ( + + )} ), - [] + [] // no dependencies required ); const [newEventModal, setNewEventModal] = useState(false); // 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 const [selectedEvent, setSelectedEvent] = useState(null); @@ -461,6 +814,10 @@ export default function Schedule() { const [endDate, setEndDate] = useState(new Date()); const [show, setShow] = useState(true); // determines whether the datetime-picker modal should open or remain closed + // hook to handle displaying custom recurrence modals + const [showCustomRecurrenceModal, setShowCustomRecurrenceModal] = useState(false); + const [customSelectedDays, setCustomSelectedDays] = useState([]); // stores the custom days the event should be repeated + // 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; @@ -535,7 +892,9 @@ export default function Schedule() { recurrence_frequency: null, // this value should only be modified if isRecurring is set to true }); + // TODO : check if this needs to be used in the first place, otherwise, remove as part of cleanup const [retrieveUserLocation, setRetrieveUserLocation] = useState(false); + // TODO : Reference to this useState hook --> this array should be updated in the following conditions: // 1. once a new event has been created (meaning the save button within the modal has been clicked) // 2. once an existing event has been modified (a seperate modal will be used for this) @@ -546,45 +905,97 @@ export default function Schedule() { // assign a new random id for the current event // this function handles determining and assigning a new unique id to a calendar event // due to the asynchronous nature of state updates, it is ideal to only update one state at a time. - const handleCreateSaveNewEvent = () => { - const random_generated_id = Math.floor(Math.random() * 100 + 1); - console.log(random_generated_id); + const handleCreateSaveNewEvent = useCallback(async () => { + // implement logic for handling empty user input + // for title and time (description for event should be optional) + /** + * @param {Alert} defintiion + * @param {title} - Missing Information (represents the title of this particular alert in the event that title input is empty) + * @param {message} - Corresponding message to be rendered in accordance to the particular alert + */ + if (!currentEventData.title) { + Alert.alert('Missing Information', 'Please select title for your event.'); + return; + } + + if (!currentEventData.start.dateTime || !currentEventData.end.dateTime) { + Alert.alert( + 'Missing Information', + 'Please select appropriate start and end times for your event.' + ); + return; + } + + 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) => current_event.id === random_generated_id + (current_event: any) => current_event.id === uniqueId ); - console.log(`id_existence value : ${id_existence}`); + // 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: random_generated_id, - }; - - setEventList([...eventsList, updatedEvent]); - setNewEventModal(false); - return; + // 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 { - // make a recursive call onto the function (this is experimental, not entirely sure if it will work) - handleCreateSaveNewEvent(); + // 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: '', + description: '', + start: { dateTime: '' }, + end: { dateTime: '' }, + color: '#4285F4', + location: 'Not Specified', + isRecurring: false, + recurrence_frequency: null, + }); + }, [currentEventData, calculateRecurringEvents, eventsList]); // TODO : event object should also contain a description tag (alongside location, start, end and whether or not it's a recurring event, which if set to true, should have a dropdown pop up specifying how often this event ought to be recurring.) // TODO : the classes should automatically be added to the schedule (although that's something to consider later) but this should be an unique standout feature of it's own // define the function that will handle the creation of new events // note that the modal for this should be different from the existing event modal // NOTE : it would be more appropriate to call this functon "renderNewEventModal" + + // TLDR : this function gets triggered when the "+" icon gets selected const handleCreateNewEvent = () => { // for now the only behavior we want is for the useState hook variable // when this modal view is set to true, the modal will be displayed // reset all the previous relevant data (so that the modal doesn't render old data that was previously entered by user) + // TODO : issue with DRY, fix this setCurrentEventData({ id: -1, title: '', @@ -599,7 +1010,7 @@ export default function Schedule() { setNewEventModal(true); }; - const renderDropdownItem = (item) => { + const renderDropdownItem = (item: any) => { return ( {item.label} @@ -611,11 +1022,30 @@ export default function Schedule() { }; // useCallback hook is being used as a wrapper around this reference function - const handlePressEvent = useCallback((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); - setShowExistingEventModal(true); - }, []); + // simple logic for handling recurring ev + 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); + setShowExistingEventModal(true); + }, + [eventsList] + ); // prototype of the data that needs to be sent out to the datbase const events_payload = { @@ -624,16 +1054,27 @@ export default function Schedule() { }; // 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(`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]); + console.log('showCustomRecurrencModal : ', showCustomRecurrenceModal); + }, [showCustomRecurrenceModal]); 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 + */} + {isLoading && ( + + + + )} @@ -674,15 +1130,18 @@ export default function Schedule() { {showExistingEventModal && ( + // TODO : fix the issue with text input not working and changing the current functions into reusable reference functions { + setDeleteEventModal(false); + }} + handleOnPressDeleteConfirmation={async () => { + // logic for deleting a particular event + const updatedEvents = await eventsList.filter( + (event: any) => 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); + setDeleteEventModal(false); + setShowExistingEventModal(false); // close the event + }} + delete_event_modal={deleteEventModal} + end_time={endDate} start_time={startDate} // pass in the start and end date for the date time picker current_event={selectedEvent} visibillity_state={showExistingEventModal} @@ -699,28 +1176,71 @@ export default function Schedule() { isEditable={isModalEditable} onRequestEdit={() => setIsModalEditable(true)} // NOTE : this can be changed to be reused as a reference function insted - handleOnChangeTitle={(newUserInputTitle: any) => - setCurrentEventData((prev) => ({ - ...prev, - title: newUserInputTitle, - })) - } + handleOnChangeTitle={(newUserInputTitle: any) => { + // we only want the edit to take place if the modal happens to be editable + if (isModalEditable) { + console.log(`Detected changes to user input : ${newUserInputTitle}`); + setSelectedEvent((prev: CalendarEvent) => ({ + ...prev, + title: newUserInputTitle, + })); + + // this wouldn't work due to asynchronous nature of the code + // setSelectedEvent(currentEventData); + } + }} // TODO : change to a reference function for reusabillity - handleOnChangeDescription={(newUserInputTitle: any) => - setCurrentEventData((prev) => ({ - ...prev, - title: newUserInputTitle, - })) - } - handleOnChangeStart={onChangeStart} // we can reuse the same function + // 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) => ({ + ...prevData, + description: newUserInputDescription, + })); + // setCurrentEventData((prev) => ({ + // ...prev, + // title: newUserInputTitle, + // })); + } + }} + // the function logic for the datetimepicker needs to be slighlyt different + // rather than updating the currentEventsData useState hook + // instead the setSelectedEvent useState hook needs to be updated + handleOnChangeStart={async (_event: any, selectedDate: any) => { + const currentDate = await selectedDate; + const updatedDatetime = { + dateTime: currentDate, + }; + + setStartDate(currentDate); + setSelectedEvent((previousEventData: CalendarEvent) => ({ + ...previousEventData, + start: updatedDatetime, + })); + }} + handleOnChangeEnd={async (_event: any, selectedDate: any) => { + const currentDate = await selectedDate; + const updatedDatetime = { + dateTime: currentDate, + }; + + setEndDate(currentDate); + setSelectedEvent((previousEventData: CalendarEvent) => ({ + ...previousEventData, + end: updatedDatetime, + })); + }} // TODO : change this to a reference function instead + // TODO : replace setCurrentEventData with setSelectedEvent state handleOnPressRecurring={() => - setCurrentEventData((previousData) => ({ + setSelectedEvent((previousData: CalendarEvent) => ({ ...previousData, isRecurring: !previousData.isRecurring, // toggle logic })) } dropdown_list={dropdownData} + // TODO : figure out why this isn't working handleDropdownFunction={() => { console.log('Do something'); }} @@ -731,22 +1251,38 @@ export default function Schedule() { selectedColor, // TODO : fix this })); }} - handleSaveEditedEvent={() => { - // ideally, we would have to do less work - setEventList([...eventsList, currentEventData]); + handleSaveEditedEvent={async () => { + // first we remove the old data by matching based on id + const updatedEventList = await eventsList.filter( + (event) => event.id !== selectedEvent.id + ); + console.log(`Updated event list is : ${JSON.stringify(updatedEventList)}`); + // then set the current selectedEvent related data to the updatedEventList instead + setEventList([...updatedEventList, selectedEvent]); + setShowExistingEventModal(false); + }} + handleCancelEditedEvent={() => { + // change the modal back to not being editable + setIsModalEditable(false); + setShowExistingEventModal(false); }} - handleCancelEditedEvent={() => setShowExistingEventModal(false)} onRequestDelete={() => { - // remove the event within the list whose current id matches the id of the currently selected event - const updatedEvents = eventsList.filter((event) => event.id === selectedEvent.id); - - // set the newly updated event - setEventList(updatedEvents); - setShowExistingEventModal(false); // close the event + 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 }} /> )} - + { // this is a bit confusing since it's updating the value which is an integer digit // but we want to update the label instead + + // TODO : delete later, just to check if dropdown value is being selected correctly + console.log('Dropdown changed to : ', item.label); setDropdownValue(item.value); - // note the syntax - // update the recurrence frequency - setCurrentEventData((previousStateData) => ({ - ...previousStateData, - recurrence_frequency: item.label, - })); + // modified slightly to add a conditional check to handle custom days + if (item.label === 'Custom') { + console.log( + 'This function is being triggered, should display the custom modal.' + ); + setShowCustomRecurrenceModal(true); + } else { + // note the syntax + // update the recurrence frequency + setCurrentEventData((previousStateData) => ({ + ...previousStateData, + recurrence_frequency: item.label, + })); + } }} renderLeftIcon={() => ( + {/**Conditionally render the custom recurrence modal component */} + {showCustomRecurrenceModal && ( + setShowCustomRecurrenceModal(false)} + initialSelection={customSelectedDays} + onSave={(selectedDays: any) => { + setCustomSelectedDays(selectedDays); + setCurrentEventData((prev: any) => ({ + ...prev, + recurrence_frequency: 'Custom', + customRecurrenceDays: selectedDays, // this is a new field being added? + })); + setShowCustomRecurrenceModal(false); + }} + /> + )} - {/* )} */} ); } +const calendarStyling = StyleSheet.create({ + // styling for the loading animation + // to be displayed when there's a switch in the calendar + loadingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(255,255,255,0.7)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 999, + }, +}); + const checkboxStyling = StyleSheet.create({ checkbox: { width: 20, diff --git a/src/app/(root)/(tabs)/_layout.tsx b/src/app/(root)/(tabs)/_layout.tsx index 6fba4ef..857f287 100644 --- a/src/app/(root)/(tabs)/_layout.tsx +++ b/src/app/(root)/(tabs)/_layout.tsx @@ -18,9 +18,11 @@ export default function TabLayout() { screenOptions={{ headerStyle: { backgroundColor: 'black', + // backgroundColor: 'transparent', }, tabBarStyle: { backgroundColor: 'black', + // backgroundColor: 'transparent', }, // tabBarActiveTintColor: iconColor, }} diff --git a/src/components/core/calendarModeSwitcher/index.tsx b/src/components/core/calendarModeSwitcher/index.tsx new file mode 100644 index 0000000..f77f8d6 --- /dev/null +++ b/src/components/core/calendarModeSwitcher/index.tsx @@ -0,0 +1,264 @@ +// Component to handle switching modes for the calendar +import React, { useState } from 'react'; +import { View, Text, TouchableOpacity, StyleSheet, Modal } from 'react-native'; +import { Ionicon } from '../icon'; +import { Button } from '../button/button-default'; +/** + * @CalendarModeSwitcher : A component to switch between different calendar events + * + * @param {Object} props + * @param {string} props.currentNMode - Current View Mode ('day', '3day', '4day', 'week', 'month') + * @param {Function} props.onModeChange - Callback when mode changes + */ +const CalendarModeSwitcher = ({ currentMode, onModeChange }) => { + // lists out different modes that users can switch between + const modes = [ + { id: 'day', label: 'Day', days: 1 }, + { id: '3day', label: '3 days', days: 3 }, + { id: '4day', label: '4 days', days: 4 }, + { id: 'week', label: 'Week', days: 7 }, + { id: 'month', label: 'Month', days: 30 }, + ]; + return ( + + {modes.map((mode) => ( + onModeChange(mode.id, mode.days)} + > + {mode.label} + + ))} + + ); +}; + +// TODO : replace the styling with tailwindcss during refactoring phase +// TODO : modify the starter code as needed if something looks off +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-around', + alignItems: 'center', + backgroundColor: '#f5f5f5', + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: '#e0e0e0', + }, + modeButton: { + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 20, + }, + selectedMode: { + backgroundColor: '#3498db', + }, + modeText: { + fontSize: 14, + color: '#333', + }, + selectedModeText: { + color: 'white', + fontWeight: 'bold', + }, +}); + +export default CalendarModeSwitcher; + +// seperate modal component for custom event recurrence logic + +export const CustomRecurrenceModal = ({ + visible, + onClose, + onSave, + initialSelection, +}: { + visible: boolean; // useState hook boolean type variable that will handle whether a modal should be displayed or not + + // unsure of the types of these + onClose: any; + onSave: any; + initialSelection: string[] | any; +}) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const days_of_week = [ + { id: 0, name: 'Sunday' }, + { id: 1, name: 'Monday' }, + { id: 2, name: 'Tuesday' }, + { id: 3, name: 'Wednesday' }, + { id: 4, name: 'Thursday' }, + { id: 5, name: 'Friday' }, + { id: 6, name: 'Saturday' }, + ]; + + const [selectedDays, setSelectedDays] = useState(initialSelection || []); + + /** + * + * @param dayId (a number from 0-6 representing the day of the week, with 0 representing sunday and 6 representing saturday) + * First it checks if the day is already selected using selectedDays.includes(dayId) + * If the day is already selected, it calls setSelectedDate to update the state. + * It uses the .filter() method to create a new array that excludes the clicked dayId + * This effectively deselects/removes the from the selected days list + * @returns void function, doesn't return anything + */ + const toggleDay = (dayId: number | any) => { + if (selectedDays.includes(dayId)) { + setSelectedDays(selectedDays.filter((id: any) => id !== dayId)); + } else { + setSelectedDays([...selectedDays, dayId]); + } + }; + + // TODO : continue here + // reference link : https://claude.ai/chat/ca41a962-0577-44af-987d-9e4320d828eb + + // component rendering logic + return ( + + + + Custom: + + Select days of the week you want the event to repeat + + {days_of_week.map((currentDay) => ( + toggleDay(currentDay.id)} + > + + {currentDay.name} + + {selectedDays.includes(currentDay.id) && ( + + )} + + ))} + + false using the setter + > + Cancel + + onSave(selectedDays)} + > + Save + + + + + + ); +}; + +// define styles for the view component +const customRecurrenceStyles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + }, + modalView: { + width: '80%', + backgroundColor: 'white', + borderRadius: 10, + padding: 20, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalTitle: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 10, + textAlign: 'center', + }, + modalSubtitle: { + fontSize: 14, + marginBottom: 15, + textAlign: 'center', + color: '#666', + }, + dayItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 15, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + selectedDay: { + backgroundColor: '#3498db', + borderRadius: 5, + }, + dayText: { + fontSize: 16, + color: '#333', + }, + selectedDayText: { + color: 'white', + fontWeight: 'bold', + }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, + }, +}); + +// TODO : delete later, this has been copied from Schedule.tsx: + +const ButtonStyling = StyleSheet.create({ + // general style for button that is shared between the "save" and "cancel" button + button: { + padding: 12, + borderRadius: 5, + width: '48%', + alignItems: 'center', + }, + + // styling for the save button (only the background color varies) + buttonSave: { + backgroundColor: '#3498db', + }, + + // styling for the cancel button + buttonCancel: { + backgroundColor: '#e74c3c', + }, + + // styling for text within the button itself + buttonText: { + color: 'white', + fontWeight: 'bold', + fontSize: 16, + }, +}); diff --git a/src/components/core/icon/index.tsx b/src/components/core/icon/index.tsx index ed2f462..2cc20bf 100644 --- a/src/components/core/icon/index.tsx +++ b/src/components/core/icon/index.tsx @@ -134,8 +134,10 @@ export const TabBarIcon = ({ );