Skip to content

Commit

Permalink
Merge pull request #338 from Peliah/feat/HNG-213-Component-MediaFileU…
Browse files Browse the repository at this point in the history
…pload

Feat/hng 213 component media file upload
  • Loading branch information
mrcoded authored Jul 22, 2024
2 parents 52ed28c + 98c57f3 commit db29e00
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 0 deletions.
144 changes: 144 additions & 0 deletions src/components/MediaUpload/index.tsx
Original file line number Diff line number Diff line change
@@ -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<MediaUploadProperties> = ({
onFileDeleted,
onFilesAdded,
label,
CustomButton,
}) => {
const [files, setFiles] = useState<File[]>([]);
const fileInputReference = useRef<HTMLInputElement | null>(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 (
<div>
<div className="flex w-[443px] flex-col gap-2 p-2">
<h2 className="text-sm font-medium">Media</h2>
{files.length === 0 ? (
<div className="rounded-[6px]">
<div
className="flex h-[125px] w-full flex-col items-center justify-center gap-3 rounded-[6px] border border-dashed border-slate-300 bg-zinc-50"
onDragOver={handleDragOver}
onDrop={handleDrop}
data-testid="drop-area"
>
<div className="">
<input
type="file"
name="file"
id="file"
className="inputfile absolute z-[-1] h-[0.1px] w-[0.1px] overflow-hidden opacity-0"
data-multiple-caption="{count} files selected"
multiple
ref={fileInputReference}
onChange={(event_) =>
event_.target.files && handleFilesAdded(event_.target.files)
}
/>
{CustomButton ? (
<CustomButton onClick={handleButtonClick} />
) : (
<label
htmlFor="file"
className="inline-block cursor-pointer rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium leading-6 text-gray-900 shadow-[0px_1px_18px_0px_#0A39B01F]"
>
<span className="pointer-events-none">Upload New</span>
</label>
)}
</div>
<label className="text-sm font-normal text-zinc-600">
{label}
</label>
</div>
</div>
) : (
<div>
{files.map((file, index) => (
<div
key={index}
className="image-container group relative mb-2 h-[125px] w-full"
>
{URL.createObjectURL && (
<Image
src={URL.createObjectURL(file)}
alt={file.name}
width={125}
height={125}
className="rounded-[6px]"
/>
)}
<div className="absolute left-0 top-0 h-full w-full rounded-[6px] bg-[rgba(10,10,10,0.75)] opacity-0 group-hover:opacity-100"></div>
<Button
variant={"outline"}
onClick={() => handleDeleteFile(file)}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform rounded-full bg-white p-2 text-white opacity-0 group-hover:opacity-100"
aria-label="delete file"
>
<TrashIcon color="red" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
);
};

export default MediaUpload;
77 changes: 77 additions & 0 deletions src/test/MediaUpload.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MediaUpload
onFilesAdded={onFilesAdded}
onFileDeleted={onFileDeleted}
label="Upload your media"
/>,
);

// 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(
<MediaUpload
onFilesAdded={onFilesAdded}
onFileDeleted={onFileDeleted}
label="Upload your media"
/>,
);

// 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);
});
});
});

0 comments on commit db29e00

Please sign in to comment.