fix duplicate updates
This commit is contained in:
parent
2712522bf6
commit
ee49347ff0
1 changed files with 565 additions and 449 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback, useMemo, memo } from "react";
|
||||||
import { Get } from "../../pocketbase/Get";
|
import { Get } from "../../pocketbase/Get";
|
||||||
import { Authentication } from "../../pocketbase/Authentication";
|
import { Authentication } from "../../pocketbase/Authentication";
|
||||||
import { Update } from "../../pocketbase/Update";
|
import { Update } from "../../pocketbase/Update";
|
||||||
|
@ -39,257 +39,51 @@ interface EventEditorProps {
|
||||||
onEventSaved?: () => void;
|
onEventSaved?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
// Memoize the FilePreview component
|
||||||
// State for form data and UI
|
const MemoizedFilePreview = memo(FilePreview);
|
||||||
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
|
// Define EventForm props interface
|
||||||
const get = Get.getInstance();
|
interface EventFormProps {
|
||||||
const auth = Authentication.getInstance();
|
event: Event | null;
|
||||||
const update = Update.getInstance();
|
setEvent: React.Dispatch<React.SetStateAction<Event | null>>;
|
||||||
const fileManager = FileManager.getInstance();
|
selectedFiles: Map<string, File>;
|
||||||
const sendLog = SendLog.getInstance();
|
setSelectedFiles: React.Dispatch<React.SetStateAction<Map<string, File>>>;
|
||||||
|
filesToDelete: Set<string>;
|
||||||
// Method to initialize form with event data
|
setFilesToDelete: React.Dispatch<React.SetStateAction<Set<string>>>;
|
||||||
const initializeEventData = async (eventId: string) => {
|
handlePreviewFile: (url: string, filename: string) => void;
|
||||||
try {
|
isSubmitting: boolean;
|
||||||
const eventData = await get.getOne<Event>("events", eventId);
|
fileManager: FileManager;
|
||||||
setEvent(eventData);
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
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();
|
// Create a memoized form component
|
||||||
};
|
const EventForm = memo(({
|
||||||
|
event,
|
||||||
return () => {
|
setEvent,
|
||||||
delete (window as any).openEditModal;
|
selectedFiles,
|
||||||
};
|
setSelectedFiles,
|
||||||
}, []);
|
filesToDelete,
|
||||||
|
setFilesToDelete,
|
||||||
// Handle file selection
|
handlePreviewFile,
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
isSubmitting,
|
||||||
if (e.target.files) {
|
fileManager,
|
||||||
const newFiles = Array.from(e.target.files);
|
onSubmit
|
||||||
const updatedFiles = new Map(selectedFiles);
|
}: EventFormProps): React.ReactElement => {
|
||||||
|
|
||||||
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 (
|
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">
|
<div id="editFormSection">
|
||||||
<h3 className="font-bold text-lg mb-4" id="editModalTitle">
|
<h3 className="font-bold text-lg mb-4" id="editModalTitle">
|
||||||
{event?.id ? 'Edit Event' : 'Add New Event'}
|
{event?.id ? 'Edit Event' : 'Add New Event'}
|
||||||
</h3>
|
</h3>
|
||||||
<form id="editEventForm" onSubmit={handleSubmit} className="space-y-4">
|
<form
|
||||||
|
id="editEventForm"
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isSubmitting) {
|
||||||
|
onSubmit(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<input type="hidden" id="editEventId" name="editEventId" value={event?.id || ''} />
|
<input type="hidden" id="editEventId" name="editEventId" value={event?.id || ''} />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Event Name */}
|
{/* Event Name */}
|
||||||
|
@ -413,7 +207,16 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
onChange={handleFileSelect}
|
name="editEventFiles"
|
||||||
|
onChange={(e) => {
|
||||||
|
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"
|
className="file-input file-input-bordered"
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
|
@ -471,7 +274,11 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost btn-xs"
|
className="btn btn-ghost btn-xs"
|
||||||
onClick={() => handleUndoFileDelete(filename)}
|
onClick={() => {
|
||||||
|
const newFilesToDelete = new Set(filesToDelete);
|
||||||
|
newFilesToDelete.delete(filename);
|
||||||
|
setFilesToDelete(newFilesToDelete);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Undo
|
Undo
|
||||||
</button>
|
</button>
|
||||||
|
@ -479,7 +286,11 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost btn-xs text-error"
|
className="btn btn-ghost btn-xs text-error"
|
||||||
onClick={() => handleFileDelete(filename)}
|
onClick={() => {
|
||||||
|
const newFilesToDelete = new Set(filesToDelete);
|
||||||
|
newFilesToDelete.add(filename);
|
||||||
|
setFilesToDelete(newFilesToDelete);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<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" />
|
<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" />
|
||||||
|
@ -533,14 +344,12 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-action">
|
{/* Action Buttons */}
|
||||||
|
<div className="modal-action mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={`btn btn-primary ${isSubmitting ? 'loading' : ''}`}
|
className={`btn btn-primary ${isSubmitting ? 'loading' : ''}`}
|
||||||
form="editEventForm"
|
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
||||||
|
@ -552,15 +361,322 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
||||||
if (modal) modal.close();
|
if (modal) modal.close();
|
||||||
}}
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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<HTMLInputElement>) => {
|
||||||
|
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<HTMLFormElement>) => {
|
||||||
|
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 = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
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 services.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;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog id="editEventModal" className="modal" onClose={(e) => {
|
||||||
|
// Prevent any default close behavior
|
||||||
|
if (isSubmitting) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{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">
|
||||||
|
<MemoizedFilePreview
|
||||||
|
url={previewUrl}
|
||||||
|
filename={previewFilename}
|
||||||
|
isModal={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="modal-box max-w-2xl">
|
||||||
|
<EventForm
|
||||||
|
event={event}
|
||||||
|
setEvent={setEvent}
|
||||||
|
selectedFiles={selectedFiles}
|
||||||
|
setSelectedFiles={setSelectedFiles}
|
||||||
|
filesToDelete={filesToDelete}
|
||||||
|
setFilesToDelete={setFilesToDelete}
|
||||||
|
handlePreviewFile={handlePreviewFile}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
fileManager={services.fileManager}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form method="dialog" className="modal-backdrop">
|
<div className="modal-backdrop" onClick={(e) => {
|
||||||
<button>close</button>
|
e.preventDefault();
|
||||||
</form>
|
if (!isSubmitting) {
|
||||||
|
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
||||||
|
if (modal) {
|
||||||
|
modal.addEventListener('close', () => {
|
||||||
|
// Do nothing on close event
|
||||||
|
}, { once: true });
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<button onClick={(e) => e.preventDefault()}>close</button>
|
||||||
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue