From 849264f4eebaa5d8c354e2f0a7fa56cbccdfbea6 Mon Sep 17 00:00:00 2001 From: anivishy Date: Mon, 10 Feb 2025 21:40:33 -0800 Subject: [PATCH 1/6] feat add google calender structure and setup --- src/app/admin/events/create/page.tsx | 334 ++++++++++++------ .../calendar/GoogleCalendarPicker.tsx | 157 ++++++++ 2 files changed, 391 insertions(+), 100 deletions(-) create mode 100644 src/app/components/calendar/GoogleCalendarPicker.tsx diff --git a/src/app/admin/events/create/page.tsx b/src/app/admin/events/create/page.tsx index 3d9c8120..65501580 100644 --- a/src/app/admin/events/create/page.tsx +++ b/src/app/admin/events/create/page.tsx @@ -22,6 +22,7 @@ import { } from "@chakra-ui/react"; import { AddIcon, ChevronDownIcon } from "@chakra-ui/icons"; import MiniCalendar from "../../../components/calendar/MiniCalendar"; +import GoogleCalendarPicker from "../../../components/calendar/GoogleCalendarPicker"; import { formatISO, parse } from "date-fns"; import { useRouter } from "next/navigation"; import { uploadFileS3Bucket } from "app/lib/clientActions"; @@ -42,7 +43,7 @@ type Group = { }; export default function Page() { - const {mutate} = useEventsAscending() + const { mutate } = useEventsAscending(); const toast = useToast(); const router = useRouter(); const [eventName, setEventName] = useState(""); @@ -50,10 +51,10 @@ export default function Page() { const [imagePreview, setImagePreview] = useState(null); const [eventType, setEventType] = useState(""); const [organizationIds, setOrganizationIds] = useState([]); - const [groupsSelected, setGroupsSelected] = useState([]) + const [groupsSelected, setGroupsSelected] = useState([]); // Specify type for group to avoid error - const {groups, isLoading, isError, mutateGroups} = useGroups() - const setGroups = useCallback((updateFunction: (groups: any[]) => any[]) => { + const { groups, isLoading, isError, mutateGroups } = useGroups(); + const setGroups = useCallback((updateFunction: (groups: any[]) => any[]) => { mutateGroups((currentGroups) => { if (currentGroups) { return updateFunction(currentGroups); @@ -65,15 +66,15 @@ export default function Page() { const [location, setLocation] = useState(""); const [language, setLanguage] = useState("Yes"); const [description, setDescription] = useState(""); - const [accessibilityAccommodation, setAccessibilityAccommodation] = useState("Yes"); + const [accessibilityAccommodation, setAccessibilityAccommodation] = + useState("Yes"); const [checkList, setChecklist] = useState("N/A"); const [eventStart, setEventStart] = useState(""); const [eventEnd, setEventEnd] = useState(""); const [activeDate, setActiveDate] = useState(""); const [eventTypes, setEventTypes] = useState([]); - const [onlyGroups, setOnlyGroups] = useState(false) - const [sendEmailInvitees, setSendEmailInvitees] = useState(false) - + const [onlyGroups, setOnlyGroups] = useState(false); + const [sendEmailInvitees, setSendEmailInvitees] = useState(false); const handleEventNameChange = (e: React.ChangeEvent) => setEventName(e.target.value); @@ -96,9 +97,9 @@ export default function Page() { //Parse and format start and end time from user input const handleTimeChange = (start: string, end: string) => { // Format for parsing input times (handle both 12-hour and 24-hour formats) - if(start && end){ + if (start && end) { const timeFormat = - start.includes("AM") || start.includes("PM") ? "h:mm a" : "HH:mm"; + start.includes("AM") || start.includes("PM") ? "h:mm a" : "HH:mm"; // Parse the start and end times as dates on the active date const parsedStartTime = parse( @@ -111,20 +112,20 @@ export default function Page() { timeFormat, new Date(`${activeDate}T00:00:00`) ); - + // Format the adjusted dates back into ISO strings const formattedStartDateTime = formatISO(parsedStartTime); const formattedEndDateTime = formatISO(parsedEndTime); // Update the state with the formatted date times setEventStart(formattedStartDateTime); setEventEnd(formattedEndDateTime); - }; - if(!start){ + } + if (!start) { setEventStart(""); - }; - if(!end){ + } + if (!end) { setEventEnd(""); - }; + } }; // Update active date upon change from MiniCalendar const handleDateChangeFromCalendar = (newDate: string) => { @@ -143,7 +144,7 @@ export default function Page() { // Handle file selection for the event cover image and set preview const handleImageChange = async (e: React.ChangeEvent) => { - setPreselected(false) + setPreselected(false); const file = e.target.files ? e.target.files[0] : null; if (file) { const reader = new FileReader(); @@ -157,7 +158,6 @@ export default function Page() { } }; - // Throw a Toast when event details are not complete and makes a post request to create event if details are complete const handleCreateEvent = async () => { debugger; @@ -177,25 +177,36 @@ export default function Page() { isClosable: true, }); return; - } - else if ( - eventStart === "" || eventEnd === "" || activeDate === "" - ){ + } + + // Validate recurring event dates + if (!recurringOptions.startDate || !recurringOptions.endDate) { + toast({ + title: "Error", + description: "Event start and end dates are not set", + status: "error", + duration: 2500, + isClosable: true, + }); + return; + } + + if (recurringOptions.endDate < recurringOptions.startDate) { toast({ title: "Error", - description: "Event date and time are not set", + description: "End date cannot be before start date", status: "error", duration: 2500, isClosable: true, }); return; } - else if( - eventEnd < eventStart - ){ + + // Validate weekly recurrence + if (recurringOptions.frequency === 'weekly' && recurringOptions.daysOfWeek.length === 0) { toast({ title: "Error", - description: "End time is before start time", + description: "Please select at least one day for weekly recurring events", status: "error", duration: 2500, isClosable: true, @@ -206,26 +217,25 @@ export default function Page() { // Try to upload image const file = fileInputRef?.current?.files?.[0] ?? null; let imageurl = null; - + if (file || preselected) { - if (!preselected){ - imageurl = await uploadFileS3Bucket(file); + if (!preselected) { + imageurl = await uploadFileS3Bucket(file); if (!imageurl) { - console.error("Failed to create the event: image upload."); - toast({ + console.error("Failed to create the event: image upload."); + toast({ title: "Error", description: "Failed to create the event", status: "error", duration: 2500, isClosable: true, - }); - return; - } } - else{ - imageurl = imagePreview - } + }); + return; } - + } else { + imageurl = imagePreview; + } + } const eventData = { eventName, @@ -236,16 +246,57 @@ export default function Page() { description, wheelchairAccessible: accessibilityAccommodation === "Yes", spanishSpeakingAccommodation: language === "Yes", - startTime: eventStart, - endTime: eventEnd, + startTime: recurringOptions.startDate, + endTime: recurringOptions.endDate, volunteerEvent: eventType === "Volunteer", - groupsAllowed: groupsSelected.map(group => group._id as string), - groupsOnly: onlyGroups + groupsAllowed: groupsSelected.map((group) => group._id as string), + groupsOnly: onlyGroups, + recurring: { + frequency: recurringOptions.frequency, + daysOfWeek: recurringOptions.daysOfWeek, + endDate: recurringOptions.endDate + } + }; + + const createGoogleCalendarEvent = async (eventData: any) => { + try { + const response = await fetch('/api/google-calendar', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + summary: eventData.eventName, + location: eventData.location, + description: eventData.description, + start: { + dateTime: eventData.startTime, + timeZone: 'America/Los_Angeles', // Adjust to your timezone + }, + end: { + dateTime: eventData.endTime, + timeZone: 'America/Los_Angeles', // Adjust to your timezone + }, + recurrence: eventData.recurring ? [ + `RRULE:FREQ=${eventData.recurring.frequency.toUpperCase()};UNTIL=${eventData.recurring.endDate.replace(/-/g, '')}T235959Z${eventData.recurring.daysOfWeek.length > 0 ? ';BYDAY=' + eventData.recurring.daysOfWeek.join(',') : ''}` + ] : undefined, + }), + }); + + if (!response.ok) { + throw new Error('Failed to create Google Calendar event'); + } + + return await response.json(); + } catch (error) { + console.error('Error creating Google Calendar event:', error); + throw error; + } }; // Attempt to create event via API and handle response try { - + // Create event in your database const response = await fetch("/api/events", { method: "POST", headers: { @@ -258,24 +309,46 @@ export default function Page() { throw new Error("HTTP error! status: $(response.status)"); } - const event: IEvent= await response.json(); - // send confirmation email if button was checked - if (sendEmailInvitees){ - const res = await fetch('/api/events/' + event._id + "/groups/confirmation", - {method: 'POST', - body: JSON.stringify({groupIds: groupsSelected.flatMap(group => group._id)})}) - if (!res){ - toast() + const event: IEvent = await response.json(); + + // Create Google Calendar event + try { + await createGoogleCalendarEvent(eventData); + } catch (calendarError) { + console.error("Failed to create Google Calendar event:", calendarError); + // Optionally show a warning toast but continue with the flow toast({ - title: "Error", - description: "Failed to send emails.", - status: "error", - duration: 2500, - isClosable: true, - }); - } + title: "Warning", + description: "Event created but failed to sync with Google Calendar", + status: "warning", + duration: 5000, + isClosable: true, + }); + } + + // send confirmation email if button was checked + if (sendEmailInvitees) { + const res = await fetch( + "/api/events/" + event._id + "/groups/confirmation", + { + method: "POST", + body: JSON.stringify({ + groupIds: groupsSelected.flatMap((group) => group._id), + }), + } + ); + if (!res) { + toast({ + title: "Error", + description: "Failed to send emails.", + status: "error", + duration: 2500, + isClosable: true, + }); + } } - mutate() + + mutate(); toast({ title: "Event Created", description: "Your event has been successfully created.", @@ -294,7 +367,7 @@ export default function Page() { isClosable: true, }); } - }; +}; const handleCreateNewGroup = async (groupName: string) => { const groupData = { @@ -352,16 +425,15 @@ export default function Page() { // Fetch groups data on component mount useEffect(() => { - if (isError){ - toast({ - title: "Error", - description: "Failed to fetch groups", - status: "error", - duration: 2500, - isClosable: true, - }); + if (isError) { + toast({ + title: "Error", + description: "Failed to fetch groups", + status: "error", + duration: 2500, + isClosable: true, + }); } - }, [isError]); // Fetching different event types @@ -381,27 +453,43 @@ export default function Page() { fetchEventTypes(); }, []); - - + interface RecurringOptions { + startDate: string; + endDate: string; + daysOfWeek: string[]; + frequency: string; + } + + const [recurringOptions, setRecurringOptions] = useState({ + startDate: '', + endDate: '', + daysOfWeek: [], // initialize as empty string array + frequency: 'weekly', + }); + return ( Create New Event - {/* image uploading */} - + {/* image uploading */} + - - + + > {!imagePreview ? ( - <> + <> Upload Image - } mt="2" /> - + } + mt="2" + /> + ) : ( - Event cover preview + /> )} - + - + @@ -466,7 +561,6 @@ export default function Page() { label: type, }))} onChange={(option) => { - setEventType(option ? option.value : ""); }} chakraStyles={{ @@ -491,9 +585,9 @@ export default function Page() { value: group, label: group.group_name, }))} - value={groupsSelected.map(group => ({ - value: group, - label: group.group_name, + value={groupsSelected.map((group) => ({ + value: group, + label: group.group_name, }))} onChange={(selectedOptions) => setGroupsSelected( @@ -573,7 +667,7 @@ export default function Page() { /> - + Only Available to Selected Groups @@ -596,23 +690,46 @@ export default function Page() { }} /> - {onlyGroups && groups &&
- -
Notify Group Individuals: setSendEmailInvitees((checked) => !checked)}>
-
} + {onlyGroups && groups && ( +
+ +
+ {" "} + Notify Group Individuals:{" "} + setSendEmailInvitees((checked) => !checked)} + > +
+
+ )} Description - setDescription(e || "")} data-color-mode="light"/> + setDescription(e || "")} + data-color-mode="light" + /> Checklist - setChecklist(e || "")} data-color-mode="light"/> + setChecklist(e || "")} + data-color-mode="light" + /> @@ -621,19 +738,36 @@ export default function Page() { Date/Time {/* MiniCalendar */} - + {/* handleTimeChange(start, end)} onDateChange={(date) => handleDateChangeFromCalendar(date)} /> - + */} + + + + Date/Time + + { + setEventStart(start); + setEventEnd(end); + }} + onRecurringOptionsChange={(options: RecurringOptions) => { + setRecurringOptions(options); + }} + /> + + +