ieeeucsd-org/src/components/dashboard/Officer_EventManagement/EventEditor.tsx
2025-02-16 03:47:59 -08:00

566 lines
28 KiB
TypeScript

import { useState, useEffect } from "react";
import { Get } from "../../pocketbase/Get";
import { Authentication } from "../../pocketbase/Authentication";
import { Update } from "../../pocketbase/Update";
import { FileManager } from "../../pocketbase/FileManager";
import { SendLog } from "../../pocketbase/SendLog";
import FilePreview from "./FilePreview";
// Extend Window interface
declare global {
interface Window {
showLoading?: () => void;
hideLoading?: () => void;
}
}
interface Event {
id: string;
event_name: string;
event_description: string;
event_code: string;
location: string;
files: string[];
points_to_reward: number;
start_date: string;
end_date: string;
published: boolean;
has_food: boolean;
attendees: AttendeeEntry[];
}
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
food: string;
}
interface EventEditorProps {
onEventSaved?: () => void;
}
export default function EventEditor({ onEventSaved }: EventEditorProps) {
// State for form data and UI
const [event, setEvent] = useState<Event | null>(null);
const [previewUrl, setPreviewUrl] = useState("");
const [previewFilename, setPreviewFilename] = useState("");
const [showPreview, setShowPreview] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<Map<string, File>>(new Map());
const [filesToDelete, setFilesToDelete] = useState<Set<string>>(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();
// Method to initialize form with event data
const initializeEventData = async (eventId: string) => {
try {
const eventData = await get.getOne<Event>("events", eventId);
setEvent(eventData);
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setShowPreview(false);
} catch (error) {
console.error("Failed to fetch event data:", error);
alert("Failed to load event data. Please try again.");
}
};
// Expose initializeEventData to window
useEffect(() => {
(window as any).openEditModal = async (event?: Event) => {
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (!modal) return;
if (event?.id) {
await initializeEventData(event.id);
} else {
setEvent(null);
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setShowPreview(false);
}
modal.showModal();
};
return () => {
delete (window as any).openEditModal;
};
}, []);
// Handle file selection
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLFormElement>) => {
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 (
<dialog id="editEventModal" className="modal">
{showPreview ? (
<div className="modal-box max-w-4xl">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-3">
<button
className="btn btn-ghost btn-sm"
onClick={() => setShowPreview(false)}
>
Close
</button>
<h3 className="font-bold text-lg truncate">
{previewFilename}
</h3>
</div>
</div>
<div className="relative">
<FilePreview
url={previewUrl}
filename={previewFilename}
isModal={false}
/>
</div>
</div>
) : (
<div className="modal-box max-w-2xl">
{/* Main Edit Form Section */}
<div id="editFormSection">
<h3 className="font-bold text-lg mb-4" id="editModalTitle">
{event?.id ? 'Edit Event' : 'Add New Event'}
</h3>
<form id="editEventForm" onSubmit={handleSubmit} className="space-y-4">
<input type="hidden" id="editEventId" name="editEventId" value={event?.id || ''} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Event Name */}
<div className="form-control">
<label className="label">
<span className="label-text">Event Name</span>
<span className="label-text-alt text-error">*</span>
</label>
<input
type="text"
name="editEventName"
className="input input-bordered"
value={event?.event_name || ""}
onChange={(e) => setEvent(prev => prev ? { ...prev, event_name: e.target.value } : null)}
required
/>
</div>
{/* Event Code */}
<div className="form-control">
<label className="label">
<span className="label-text">Event Code</span>
<span className="label-text-alt text-error">*</span>
</label>
<input
type="text"
name="editEventCode"
className="input input-bordered"
value={event?.event_code || ""}
onChange={(e) => setEvent(prev => prev ? { ...prev, event_code: e.target.value } : null)}
required
/>
</div>
{/* Location */}
<div className="form-control">
<label className="label">
<span className="label-text">Location</span>
<span className="label-text-alt text-error">*</span>
</label>
<input
type="text"
name="editEventLocation"
className="input input-bordered"
value={event?.location || ""}
onChange={(e) => setEvent(prev => prev ? { ...prev, location: e.target.value } : null)}
required
/>
</div>
{/* Points to Reward */}
<div className="form-control">
<label className="label">
<span className="label-text">Points to Reward</span>
<span className="label-text-alt text-error">*</span>
</label>
<input
type="number"
name="editEventPoints"
className="input input-bordered"
value={event?.points_to_reward || 0}
onChange={(e) => setEvent(prev => prev ? { ...prev, points_to_reward: Number(e.target.value) } : null)}
min="0"
required
/>
</div>
{/* Start Date */}
<div className="form-control">
<label className="label">
<span className="label-text">Start Date</span>
<span className="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
name="editEventStartDate"
className="input input-bordered"
value={event?.start_date ? new Date(event.start_date).toISOString().slice(0, 16) : ""}
onChange={(e) => setEvent(prev => prev ? { ...prev, start_date: e.target.value } : null)}
required
/>
</div>
{/* End Date */}
<div className="form-control">
<label className="label">
<span className="label-text">End Date</span>
<span className="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
name="editEventEndDate"
className="input input-bordered"
value={event?.end_date ? new Date(event.end_date).toISOString().slice(0, 16) : ""}
onChange={(e) => setEvent(prev => prev ? { ...prev, end_date: e.target.value } : null)}
required
/>
</div>
</div>
{/* Description */}
<div className="form-control">
<label className="label">
<span className="label-text">Description</span>
<span className="label-text-alt text-error">*</span>
</label>
<textarea
name="editEventDescription"
className="textarea textarea-bordered"
value={event?.event_description || ""}
onChange={(e) => setEvent(prev => prev ? { ...prev, event_description: e.target.value } : null)}
rows={3}
required
></textarea>
</div>
{/* Files */}
<div className="form-control">
<label className="label">
<span className="label-text">Upload Files</span>
</label>
<input
type="file"
onChange={handleFileSelect}
className="file-input file-input-bordered"
multiple
/>
<div className="mt-4 space-y-2">
{/* New Files */}
{Array.from(selectedFiles.entries()).map(([name, file]) => (
<div key={name} className="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<span className="truncate">{name}</span>
<div className="flex gap-2">
<div className="badge badge-primary">New</div>
<button
type="button"
className="btn btn-ghost btn-xs text-error"
onClick={() => {
const updatedFiles = new Map(selectedFiles);
updatedFiles.delete(name);
setSelectedFiles(updatedFiles);
}}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
))}
{/* Current Files */}
{event?.files && event.files.length > 0 && (
<>
<div className="divider">Current Files</div>
{event.files.map((filename) => (
<div key={filename} className={`flex items-center justify-between p-2 bg-base-200 rounded-lg${filesToDelete.has(filename) ? " opacity-50" : ""}`}>
<span className="truncate">{filename}</span>
<div className="flex gap-2">
<button
type="button"
className="btn btn-ghost btn-xs"
onClick={() => {
if (event?.id) {
handlePreviewFile(
fileManager.getFileUrl("events", event.id, filename),
filename
);
}
}}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
</svg>
</button>
<div className="text-error">
{filesToDelete.has(filename) ? (
<button
type="button"
className="btn btn-ghost btn-xs"
onClick={() => handleUndoFileDelete(filename)}
>
Undo
</button>
) : (
<button
type="button"
className="btn btn-ghost btn-xs text-error"
onClick={() => handleFileDelete(filename)}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</button>
)}
</div>
</div>
</div>
))}
</>
)}
</div>
</div>
{/* Published */}
<div className="form-control">
<label className="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="editEventPublished"
className="toggle"
checked={event?.published || false}
onChange={(e) => setEvent(prev => prev ? { ...prev, published: e.target.checked } : null)}
/>
<span className="label-text">Publish Event</span>
</label>
<label className="label">
<span className="label-text-alt text-info">
This has to be clicked if you want to make this event available
to the public
</span>
</label>
</div>
{/* Has Food */}
<div className="form-control">
<label className="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="editEventHasFood"
className="toggle"
checked={event?.has_food || false}
onChange={(e) => setEvent(prev => prev ? { ...prev, has_food: e.target.checked } : null)}
/>
<span className="label-text">Has Food</span>
</label>
<label className="label">
<span className="label-text-alt text-info">
Check this if food will be provided at the event
</span>
</label>
</div>
</form>
</div>
<div className="modal-action">
<button
type="submit"
className={`btn btn-primary ${isSubmitting ? 'loading' : ''}`}
form="editEventForm"
disabled={isSubmitting}
>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</button>
<button
type="button"
className="btn"
onClick={() => {
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (modal) modal.close();
}}
>
Cancel
</button>
</div>
</div>
)}
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
);
}