diff --git a/src/components/MediaUpload/index.tsx b/src/components/MediaUpload/index.tsx new file mode 100644 index 000000000..4c33e1673 --- /dev/null +++ b/src/components/MediaUpload/index.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { TrashIcon } from "lucide-react"; +import Image from "next/image"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import { Button } from "../ui/button"; + +interface MediaUploadProperties { + onFilesAdded: (files: File[]) => void; + onFileDeleted: (file: File) => void; + label: string; + CustomButton?: React.FC<{ onClick: () => void }>; +} + +const MediaUpload: React.FC = ({ + onFileDeleted, + onFilesAdded, + label, + CustomButton, +}) => { + const [files, setFiles] = useState([]); + const fileInputReference = useRef(null); + + const handleDragOver = useCallback((event_: React.DragEvent) => { + event_.preventDefault(); + event_.stopPropagation(); + }, []); + + const handleFilesAdded = useCallback( + (newFiles: FileList) => { + const fileArray = Array.prototype.slice.call(newFiles); + setFiles((previousFiles) => [...previousFiles, ...fileArray]); + onFilesAdded(fileArray); + }, + [onFilesAdded], + ); + + const handleDrop = useCallback( + (event_: React.DragEvent) => { + event_.preventDefault(); + event_.stopPropagation(); + const newFiles = event_.dataTransfer.files; + handleFilesAdded(newFiles); + }, + [handleFilesAdded], + ); + + const handleDeleteFile = useCallback( + (file: File) => { + setFiles((previousFiles) => previousFiles.filter((f) => f !== file)); + onFileDeleted(file); + }, + [onFileDeleted], + ); + + const handleButtonClick = useCallback(() => { + if (fileInputReference.current) { + fileInputReference.current.click(); + } + }, []); + + useEffect(() => { + return () => { + for (const file of files) URL.revokeObjectURL(URL.createObjectURL(file)); + }; + }, [files]); + + return ( +
+
+

Media

+ {files.length === 0 ? ( +
+
+
+ + event_.target.files && handleFilesAdded(event_.target.files) + } + /> + {CustomButton ? ( + + ) : ( + + )} +
+ +
+
+ ) : ( +
+ {files.map((file, index) => ( +
+ {URL.createObjectURL && ( + {file.name} + )} +
+ +
+ ))} +
+ )} +
+
+ ); +}; + +export default MediaUpload; diff --git a/src/test/MediaUpload.test.tsx b/src/test/MediaUpload.test.tsx new file mode 100644 index 000000000..e01a87d61 --- /dev/null +++ b/src/test/MediaUpload.test.tsx @@ -0,0 +1,77 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import MediaUpload from "../components/MediaUpload/index"; + +// Ensure global.URL.createObjectURL is defined +if (!global.URL.createObjectURL) { + global.URL.createObjectURL = () => `blob:http://localhost/test-blob`; +} + +// Ensure global.URL.revokeObjectURL is defined +if (!global.URL.revokeObjectURL) { + global.URL.revokeObjectURL = () => {}; +} + +describe("mediaUpload", () => { + it("renders and allows files to be added", async () => { + expect.hasAssertions(); + const onFilesAdded = vi.fn(); + const onFileDeleted = vi.fn(); + + render( + , + ); + + // Simulate file input change + const fileInput = screen.getByLabelText(/upload new/i) as HTMLInputElement; + const file = new File(["dummy content"], "example.png", { + type: "image/png", + }); + fireEvent.change(fileInput, { target: { files: [file] } }); + + // Wait for files to be added and the component to update + await waitFor(() => { + expect(onFilesAdded).toHaveBeenCalledWith([file]); + }); + }); + + it("allows files to be deleted", async () => { + expect.hasAssertions(); + const onFilesAdded = vi.fn(); + const onFileDeleted = vi.fn(); + + render( + , + ); + + // Simulate file input change + const fileInput = screen.getByLabelText(/upload new/i) as HTMLInputElement; + const file = new File(["dummy content"], "example.png", { + type: "image/png", + }); + fireEvent.change(fileInput, { target: { files: [file] } }); + + // Wait for files to be added + await waitFor(() => { + expect(onFilesAdded).toHaveBeenCalledWith([file]); + }); + + // Simulate delete button click outside of waitFor + const deleteButton = screen.getByRole("button", { name: /delete file/i }); + fireEvent.click(deleteButton); + + // Wait for file to be deleted and the component to update + await waitFor(() => { + expect(onFileDeleted).toHaveBeenCalledWith(file); + }); + }); +});