diff --git a/src/components/dashboard/Officer_EventManagement/EventEditor.tsx b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx index 9c2a641..3c809df 100644 --- a/src/components/dashboard/Officer_EventManagement/EventEditor.tsx +++ b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback, useMemo, memo } from "react"; import { Get } from "../../pocketbase/Get"; import { Authentication } from "../../pocketbase/Authentication"; import { Update } from "../../pocketbase/Update"; @@ -39,6 +39,338 @@ interface EventEditorProps { onEventSaved?: () => void; } +// Memoize the FilePreview component +const MemoizedFilePreview = memo(FilePreview); + +// Define EventForm props interface +interface EventFormProps { + event: Event | null; + setEvent: React.Dispatch>; + selectedFiles: Map; + setSelectedFiles: React.Dispatch>>; + filesToDelete: Set; + setFilesToDelete: React.Dispatch>>; + handlePreviewFile: (url: string, filename: string) => void; + isSubmitting: boolean; + fileManager: FileManager; + onSubmit: (e: React.FormEvent) => void; +} + +// Create a memoized form component +const EventForm = memo(({ + event, + setEvent, + selectedFiles, + setSelectedFiles, + filesToDelete, + setFilesToDelete, + handlePreviewFile, + isSubmitting, + fileManager, + onSubmit +}: EventFormProps): React.ReactElement => { + return ( +
+

+ {event?.id ? 'Edit Event' : 'Add New Event'} +

+
{ + e.preventDefault(); + if (!isSubmitting) { + onSubmit(e); + } + }} + > + +
+ {/* Event Name */} +
+ + setEvent(prev => prev ? { ...prev, event_name: e.target.value } : null)} + required + /> +
+ + {/* Event Code */} +
+ + setEvent(prev => prev ? { ...prev, event_code: e.target.value } : null)} + required + /> +
+ + {/* Location */} +
+ + setEvent(prev => prev ? { ...prev, location: e.target.value } : null)} + required + /> +
+ + {/* Points to Reward */} +
+ + setEvent(prev => prev ? { ...prev, points_to_reward: Number(e.target.value) } : null)} + min="0" + required + /> +
+ + {/* Start Date */} +
+ + setEvent(prev => prev ? { ...prev, start_date: e.target.value } : null)} + required + /> +
+ + {/* End Date */} +
+ + setEvent(prev => prev ? { ...prev, end_date: e.target.value } : null)} + required + /> +
+
+ + {/* Description */} +
+ + +
+ + {/* Files */} +
+ + { + if (e.target.files) { + const newFiles = new Map(selectedFiles); + Array.from(e.target.files).forEach(file => { + newFiles.set(file.name, file); + }); + setSelectedFiles(newFiles); + } + }} + className="file-input file-input-bordered" + multiple + /> +
+ {/* New Files */} + {Array.from(selectedFiles.entries()).map(([name, file]) => ( +
+ {name} +
+
New
+ +
+
+ ))} + + {/* Current Files */} + {event?.files && event.files.length > 0 && ( + <> +
Current Files
+ {event.files.map((filename) => ( +
+ {filename} +
+ +
+ {filesToDelete.has(filename) ? ( + + ) : ( + + )} +
+
+
+ ))} + + )} +
+
+ + {/* Published */} +
+ + +
+ + {/* Has Food */} +
+ + +
+ + {/* Action Buttons */} +
+ + +
+
+
+ ); +}); + export default function EventEditor({ onEventSaved }: EventEditorProps) { // State for form data and UI const [event, setEvent] = useState(null); @@ -49,17 +381,209 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) { const [filesToDelete, setFilesToDelete] = useState>(new Set()); const [isSubmitting, setIsSubmitting] = useState(false); - // Initialize services - const get = Get.getInstance(); - const auth = Authentication.getInstance(); - const update = Update.getInstance(); - const fileManager = FileManager.getInstance(); - const sendLog = SendLog.getInstance(); + // Memoize service instances + const services = useMemo(() => ({ + get: Get.getInstance(), + auth: Authentication.getInstance(), + update: Update.getInstance(), + fileManager: FileManager.getInstance(), + sendLog: SendLog.getInstance() + }), []); + + // Memoize handlers + const handleFileSelect = useCallback((e: React.ChangeEvent) => { + if (e.target.files) { + setSelectedFiles(prev => { + const newFiles = new Map(prev); + Array.from(e.target.files!).forEach(file => { + newFiles.set(file.name, file); + }); + return newFiles; + }); + } + }, []); + + const handleFileDelete = useCallback((filename: string) => { + if (confirm("Are you sure you want to remove this file?")) { + setFilesToDelete(prev => new Set([...prev, filename])); + } + }, []); + + const handleUndoFileDelete = useCallback((filename: string) => { + setFilesToDelete(prev => { + const newSet = new Set(prev); + newSet.delete(filename); + return newSet; + }); + }, []); + + const handlePreviewFile = useCallback((url: string, filename: string) => { + setPreviewUrl(url); + setPreviewFilename(filename); + setShowPreview(true); + }, []); + + // Optimize form submission + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + if (isSubmitting) return; // Early return if already submitting + + const form = e.target as HTMLFormElement; + const formData = new FormData(form); + const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement; + const cancelButton = form.querySelector('button[type="button"]') as HTMLButtonElement; + + setIsSubmitting(true); + if (submitButton) submitButton.disabled = true; + if (cancelButton) cancelButton.disabled = true; + + try { + window.showLoading?.(); + const eventData = { + event_name: formData.get("editEventName"), + event_code: formData.get("editEventCode"), + event_description: formData.get("editEventDescription"), + location: formData.get("editEventLocation"), + points_to_reward: Number(formData.get("editEventPoints")), + start_date: new Date(formData.get("editEventStartDate") as string).toISOString(), + end_date: new Date(formData.get("editEventEndDate") as string).toISOString(), + published: formData.get("editEventPublished") === "on", + has_food: formData.get("editEventHasFood") === "on", + }; + + const pb = services.auth.getPocketBase(); + + if (event?.id) { + // Optimize update by fetching files in parallel + const currentEvent = await pb.collection("events").getOne(event.id); + const currentFiles = currentEvent.files || []; + const remainingFiles = currentFiles.filter( + (filename: string) => !filesToDelete.has(filename) + ); + + const updateFormData = new FormData(); + Object.entries(eventData).forEach(([key, value]) => { + updateFormData.append(key, String(value)); + }); + + // Fetch all remaining files in parallel + const filePromises = remainingFiles.map(async (filename: string) => { + try { + const response = await fetch( + services.fileManager.getFileUrl("events", event.id, filename) + ); + const blob = await response.blob(); + return new File([blob], filename, { type: blob.type }); + } catch (error) { + console.error(`Failed to fetch file ${filename}:`, error); + return null; + } + }); + + const existingFiles = (await Promise.all(filePromises)).filter( + (file): file is File => file !== null + ); + + [...existingFiles, ...Array.from(selectedFiles.values())].forEach( + (file: File) => { + updateFormData.append("files", file); + } + ); + + await Promise.all([ + pb.collection("events").update(event.id, updateFormData), + services.sendLog.send( + "update", + "event", + `Updated event: ${eventData.event_name}` + ), + ...Array.from(filesToDelete).map(filename => + services.sendLog.send( + "delete", + "event_file", + `Deleted file ${filename} from event ${eventData.event_name}` + ) + ) + ]); + } else { + const createFormData = new FormData(); + Object.entries(eventData).forEach(([key, value]) => { + createFormData.append(key, String(value)); + }); + createFormData.append("attendees", JSON.stringify([])); + Array.from(selectedFiles.values()).forEach((file: File) => { + createFormData.append("files", file); + }); + + await Promise.all([ + pb.collection("events").create(createFormData), + services.sendLog.send( + "create", + "event", + `Created event: ${eventData.event_name}` + ) + ]); + } + + // Show success state briefly + if (submitButton) { + submitButton.classList.remove("btn-disabled"); + submitButton.classList.add("btn-success"); + submitButton.innerHTML = ` + + + + `; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + // Reset form and close modal + setEvent(null); + setSelectedFiles(new Map()); + setFilesToDelete(new Set()); + setShowPreview(false); + form.reset(); + + // Call onEventSaved before closing modal + onEventSaved?.(); + + // Close modal last + const modal = document.getElementById("editEventModal") as HTMLDialogElement; + if (modal) { + modal.addEventListener('close', () => { + // Do nothing on close event + }, { once: true }); + modal.close(); + } + } catch (error) { + console.error("Failed to save event:", error); + if (submitButton) { + submitButton.classList.remove("btn-disabled"); + submitButton.classList.add("btn-error"); + submitButton.innerHTML = ` + + + + `; + await new Promise(resolve => setTimeout(resolve, 2000)); + } + alert("Failed to save event. Please try again."); + } finally { + setIsSubmitting(false); + if (submitButton) { + submitButton.disabled = false; + submitButton.classList.remove("btn-disabled", "btn-success", "btn-error"); + submitButton.innerHTML = 'Save Changes'; + } + if (cancelButton) cancelButton.disabled = false; + window.hideLoading?.(); + } + }, [event, selectedFiles, filesToDelete, services, onEventSaved, isSubmitting]); // Method to initialize form with event data const initializeEventData = async (eventId: string) => { try { - const eventData = await get.getOne("events", eventId); + const eventData = await services.get.getOne("events", eventId); setEvent(eventData); setSelectedFiles(new Map()); setFilesToDelete(new Set()); @@ -93,172 +617,13 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) { }; }, []); - // Handle file selection - const handleFileSelect = (e: React.ChangeEvent) => { - if (e.target.files) { - const newFiles = Array.from(e.target.files); - const updatedFiles = new Map(selectedFiles); - - newFiles.forEach(file => { - updatedFiles.set(file.name, file); - }); - - setSelectedFiles(updatedFiles); - } - }; - - // Handle file deletion - const handleFileDelete = (filename: string) => { - if (confirm("Are you sure you want to remove this file?")) { - const updatedFilesToDelete = new Set(filesToDelete); - updatedFilesToDelete.add(filename); - setFilesToDelete(updatedFilesToDelete); - } - }; - - // Handle file deletion undo - const handleUndoFileDelete = (filename: string) => { - const updatedFilesToDelete = new Set(filesToDelete); - updatedFilesToDelete.delete(filename); - setFilesToDelete(updatedFilesToDelete); - }; - - // Handle file preview - const handlePreviewFile = (url: string, filename: string) => { - setPreviewUrl(url); - setPreviewFilename(filename); - setShowPreview(true); - }; - - // Handle form submission - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const form = e.target as HTMLFormElement; - const formData = new FormData(form); - - setIsSubmitting(true); - try { - window.showLoading?.(); - const eventData = { - event_name: formData.get("editEventName"), - event_code: formData.get("editEventCode"), - event_description: formData.get("editEventDescription"), - location: formData.get("editEventLocation"), - points_to_reward: Number(formData.get("editEventPoints")), - start_date: new Date(formData.get("editEventStartDate") as string).toISOString(), - end_date: new Date(formData.get("editEventEndDate") as string).toISOString(), - published: formData.get("editEventPublished") === "on", - has_food: formData.get("editEventHasFood") === "on", - }; - - const pb = auth.getPocketBase(); - - if (event?.id) { - // Update existing event - const currentEvent = await pb.collection("events").getOne(event.id); - const currentFiles = currentEvent.files || []; - - // Filter out files marked for deletion - const remainingFiles = currentFiles.filter( - (filename: string) => !filesToDelete.has(filename) - ); - - // Create FormData for update - const updateFormData = new FormData(); - - // Add event data - Object.entries(eventData).forEach(([key, value]) => { - updateFormData.append(key, String(value)); - }); - - // Add remaining and new files - const filePromises = remainingFiles.map(async (filename: string) => { - try { - const response = await fetch( - fileManager.getFileUrl("events", event.id, filename) - ); - const blob = await response.blob(); - return new File([blob], filename, { type: blob.type }); - } catch (error) { - console.error(`Failed to fetch file ${filename}:`, error); - return null; - } - }); - - const existingFiles = (await Promise.all(filePromises)).filter( - (file): file is File => file !== null - ); - - [...existingFiles, ...Array.from(selectedFiles.values())].forEach( - (file: File) => { - updateFormData.append("files", file); - } - ); - - await pb.collection("events").update(event.id, updateFormData); - await sendLog.send( - "update", - "event", - `Updated event: ${eventData.event_name}` - ); - - // Log file deletions - for (const filename of filesToDelete) { - await sendLog.send( - "delete", - "event_file", - `Deleted file ${filename} from event ${eventData.event_name}` - ); - } - } else { - // Create new event - const createFormData = new FormData(); - - // Add event data - Object.entries(eventData).forEach(([key, value]) => { - createFormData.append(key, String(value)); - }); - - // Initialize empty attendees array - createFormData.append("attendees", JSON.stringify([])); - - // Add new files - Array.from(selectedFiles.values()).forEach((file: File) => { - createFormData.append("files", file); - }); - - await pb.collection("events").create(createFormData); - await sendLog.send( - "create", - "event", - `Created event: ${eventData.event_name}` - ); - } - - // Close modal and reset state - const modal = document.getElementById("editEventModal") as HTMLDialogElement; - if (modal) modal.close(); - - setEvent(null); - setSelectedFiles(new Map()); - setFilesToDelete(new Set()); - setShowPreview(false); - form.reset(); - - // Notify parent component - onEventSaved?.(); - - } catch (error) { - console.error("Failed to save event:", error); - alert("Failed to save event. Please try again."); - } finally { - setIsSubmitting(false); - window.hideLoading?.(); - } - }; - return ( - + { + // Prevent any default close behavior + if (isSubmitting) { + e.preventDefault(); + } + }}> {showPreview ? (
@@ -275,7 +640,7 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
- ) : (
- {/* Main Edit Form Section */} -
-

- {event?.id ? 'Edit Event' : 'Add New Event'} -

-
- -
- {/* Event Name */} -
- - setEvent(prev => prev ? { ...prev, event_name: e.target.value } : null)} - required - /> -
- - {/* Event Code */} -
- - setEvent(prev => prev ? { ...prev, event_code: e.target.value } : null)} - required - /> -
- - {/* Location */} -
- - setEvent(prev => prev ? { ...prev, location: e.target.value } : null)} - required - /> -
- - {/* Points to Reward */} -
- - setEvent(prev => prev ? { ...prev, points_to_reward: Number(e.target.value) } : null)} - min="0" - required - /> -
- - {/* Start Date */} -
- - setEvent(prev => prev ? { ...prev, start_date: e.target.value } : null)} - required - /> -
- - {/* End Date */} -
- - setEvent(prev => prev ? { ...prev, end_date: e.target.value } : null)} - required - /> -
-
- - {/* Description */} -
- - -
- - {/* Files */} -
- - -
- {/* New Files */} - {Array.from(selectedFiles.entries()).map(([name, file]) => ( -
- {name} -
-
New
- -
-
- ))} - - {/* Current Files */} - {event?.files && event.files.length > 0 && ( - <> -
Current Files
- {event.files.map((filename) => ( -
- {filename} -
- -
- {filesToDelete.has(filename) ? ( - - ) : ( - - )} -
-
-
- ))} - - )} -
-
- - {/* Published */} -
- - -
- - {/* Has Food */} -
- - -
-
-
- -
- - -
+
)} -
- -
+
{ + e.preventDefault(); + if (!isSubmitting) { + const modal = document.getElementById("editEventModal") as HTMLDialogElement; + if (modal) { + modal.addEventListener('close', () => { + // Do nothing on close event + }, { once: true }); + modal.close(); + } + } + }}> + +
); }