Skip to content

Commit

Permalink
feat(products): Implement product update and deletion by sellers
Browse files Browse the repository at this point in the history
- a seller should be able to update a product from their collection
- a seller should be able to delete a product from their collection

Delivers #187419126
  • Loading branch information
Heisjabo committed Jul 6, 2024
1 parent 9652708 commit 21d101c
Show file tree
Hide file tree
Showing 20 changed files with 929 additions and 133 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = {
"react/no-unescaped-entities": "off",
'@typescript-eslint/no-explicit-any': 0,
"react/prop-types": "off",
"jsx-a11y/control-has-associated-label": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
Expand Down
7 changes: 2 additions & 5 deletions src/__test__/addProducts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ describe("FileUpload component", () => {
render(
<FileUpload onDrop={onDropMock} remove={removeMock} files={filesMock} />,
);

expect(screen.getByText(/example.png/)).toBeInTheDocument();
expect(screen.getByText(/example.png/)).toHaveClass("text-gray-500");
expect(screen.getByRole("button", { name: /remove/i })).toBeInTheDocument();
});

Expand Down Expand Up @@ -158,11 +155,11 @@ describe("test seller dashboard components", () => {

fireEvent.click(select);

const categoryOption = screen.getByText("Category 1");
const categoryOption = screen.getByText("Select a category");
expect(categoryOption).toBeDefined();

fireEvent.click(categoryOption);
expect(select).toHaveTextContent("Category 1");
expect(select).toHaveTextContent("Select a category");
});

it("should render the TextInput with label, placeholder, and no error", () => {
Expand Down
101 changes: 101 additions & 0 deletions src/__test__/productActions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import "@testing-library/jest-dom";
import {
fireEvent, render, screen, waitFor,
} from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";

import store from "../redux/store";
import { fetchProducts } from "../redux/reducers/productsSlice";

jest.mock("react-dropzone", () => ({
useDropzone: jest.fn(),
}));

jest.mock("../redux/api/productsApiSlice", () => ({
addProduct: jest.fn(),
updateProduct: jest.fn(),
deleteProduct: jest.fn(),
}));

jest.mock("react-toastify", () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));

jest.mock("../components/dashboard/ConfirmModal", () => () => (
<div>ConfirmModal</div>
));
jest.mock("../components/dashboard/Spinner", () => () => <div>Spinner</div>);
jest.mock(
"../components/dashboard/ToggleSwitch",
() => ({ checked, onChange }) => (
<div onClick={onChange}>
ToggleSwitch
{checked ? "On" : "Off"}
</div>
),
);

const mockProducts = [
{
id: 1,
name: "Product 1",
stockQuantity: 10,
expiryDate: "2024-12-31T00:00:00.000Z",
price: 100,
category: { name: "Electronics" },
isAvailable: true,
images: ["image1.png"],
},
{
id: 2,
name: "Product 2",
stockQuantity: 5,
expiryDate: "2025-06-30T00:00:00.000Z",
price: 50,
category: { name: "Fashion" },
isAvailable: true,
images: ["image2.png"],
},
];

global.URL.createObjectURL = jest.fn();

describe("Products slice tests", () => {
it("should handle products initial state", () => {
expect(store.getState().products).toEqual({
loading: false,
data: [],
error: null,
});
});

it("should handle products pending", () => {
store.dispatch(fetchProducts.pending(""));
expect(store.getState().products).toEqual({
loading: true,
data: [],
error: null,
});
});

it("should handle products fulfilled", () => {
const mockData = { message: "success" };
// @ts-ignore
store.dispatch(fetchProducts.fulfilled(mockData, "", {}));
expect(store.getState().products).toEqual({
loading: false,
data: mockData,
error: null,
});
});

it("should handle products fetch rejected", () => {
// @ts-ignore
store.dispatch(fetchProducts.rejected(null, "", {}));
expect(store.getState().products.error).toEqual("Rejected");
});
});
50 changes: 50 additions & 0 deletions src/components/dashboard/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";

interface ConfirmDeleteModalProps {
onConfirm: () => void;
onCancel: () => void;
message: string;
product: any;
loading: boolean;
}

const ConfirmModal: React.FC<ConfirmDeleteModalProps> = ({
onConfirm,
onCancel,
message,
product,
loading,
}) => (
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white py-8 px-6 rounded-lg duration-75 animate-fadeIn">
<div className="flex flex-col gap-2 mb-3">
<h2 className="text-lg">{message}</h2>
<p className="text-gray-700">
<strong className="text-black">
{product.name}
.
</strong>
{' '}
This can't be
undone
</p>
</div>
<div className="flex justify-end">
<button
className="border border-blue-700 text-blue-700 px-4 py-2 rounded mr-2"
onClick={onCancel}
>
Cancel
</button>
<button
className="bg-red-600 text-white px-4 py-2 rounded"
onClick={onConfirm}
>
{loading ? "Loading..." : "Delete"}
</button>
</div>
</div>
</div>
);

export default ConfirmModal;
16 changes: 8 additions & 8 deletions src/components/dashboard/CustomSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,28 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
testId,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(defaultValue);
const loading = useSelector((state: RootState) => state.categories.loading);

const handleOptionClick = (option) => {
setSelectedOption(option.name);
const handleOptionClick = (option: any) => {
setIsOpen(false);
onSelect(option);
if (option !== "Category") {
onSelect(option);
}
};

return (
<div className="relative min-w-40">
<div
className="appearance-none rounded-[8px] border-[0.5px] border-[#E5E5E5] px-4 py-3 bg-white text-dark-gray flex items-center justify-between text-md gap-2 border-dark-gray text-dark-gray cursor-pointer"
className="appearance-none rounded-[8px] border-[0.5px] border-[#E5E5E5] px-4 py-3 bg-white flex items-center justify-between text-md gap-2 text-black cursor-pointer"
onClick={() => setIsOpen(!isOpen)}
data-testid={testId}
>
{selectedOption}
{defaultValue}
{' '}
<FaAngleDown />
</div>
{isOpen && (
<div className="absolute category-options top-full h-64 overflow-y-scroll w-full left-0 z-10 dark:bg-secondary-black shadow rounded-[8px] py-3 bg-white text-[#161616] border-dark-gray mt-1">
<div className="absolute category-options top-full max-h-64 overflow-y-scroll w-full left-0 z-10 shadow rounded-[8px] py-3 bg-white text-[#161616] border-gray-800 mt-1">
{loading ? (
<div className="flex items-center justify-center select-none py-6">
loading categories...
Expand All @@ -52,7 +52,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
options.map((option) => (
<div
key={option.id}
className="px-4 py-2 text-dark-gray text-md hover:bg-[#F7F8FA] cursor-pointer"
className="px-4 py-2 text-gray-700 text-md hover:bg-[#F7F8FA] cursor-pointer"
onClick={() => handleOptionClick(option)}
>
{option.name}
Expand Down
86 changes: 59 additions & 27 deletions src/components/dashboard/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ interface FileUploadProps {
onDrop: (acceptedFiles: File[]) => void;
remove: (file: File, event: React.MouseEvent<HTMLButtonElement>) => void;
files: File[];
existingImages?: string[];
removeExistingImage: (
index: number,
image: string,
event: React.MouseEvent<HTMLButtonElement>,
) => void;
}

const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(
({ onDrop, remove, files }, ref) => {
({
onDrop, remove, files, existingImages, removeExistingImage,
}, ref) => {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
Expand All @@ -22,38 +30,62 @@ const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(
return (
<div
{...getRootProps()}
className={`border-dashed border-2 rounded-[8px] p-4 flex flex-col bg-white text-primary justify-center items-center cursor-pointer ${
className={`border-dashed border-2 rounded-[8px] p-4 flex flex-col bg-white text-primary justify-center items-center cursor-pointer ${
isDragActive ? "border-primary" : "border-[#687588]"
}`}
>
<input {...getInputProps()} ref={ref} />
{files.length > 0 ? (
files.map((file) => (
<div
key={file.name}
className="flex items-center w-full justify-between"
>
<img
src={URL.createObjectURL(file)}
alt="Preview"
className="mb-2 w-16 h-16 object-cover rounded-lg"
/>
<p className="text-gray-500">{`${file.name.slice(0, 15)}...`}</p>
<button
type="button"
onClick={(e) => remove(file, e)}
className="text-red-500"
<div className=" max-h-80 w-full overflow-y-scroll px-3">
{existingImages && existingImages.length > 0 && (
<>
{existingImages.map((image, index) => (
<div
key={index}
className="flex items-center w-full justify-between"
>
<img
src={image}
alt="Preview"
className="mb-2 w-16 h-16 object-cover rounded-lg"
/>
<button
type="button"
onClick={(e) => removeExistingImage(index, image, e)}
className="text-red-500"
>
Remove
</button>
</div>
))}
</>
)}
{files.length > 0 ? (
files.map((file, index) => (
<div
key={index}
className="flex items-center w-full justify-between"
>
Remove
</button>
<img
src={URL.createObjectURL(file)}
alt="Preview"
className="mb-2 w-16 h-16 object-cover rounded-lg"
/>
<button
type="button"
onClick={(e) => remove(file, e)}
className="text-red-500"
>
Remove
</button>
</div>
))
) : existingImages && existingImages.length > 0 ? null : (
<div className="flex items-center flex-col gap-2">
<FaImages className="text-gray-500 text-4xl mb-2" />
<p className="text-gray-500">Browse Images...</p>
</div>
))
) : (
<div className="flex items-center flex-col gap-2">
<FaImages className="text-gray-500 text-4xl mb-2" />
<p className="text-gray-500">Browse Images...</p>
</div>
)}
)}
</div>
</div>
);
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface HeaderProps {
}

const Header: React.FC<HeaderProps> = ({ toggleSidebar }) => (
<header className="flex lg:w-[80%] lg:ml-[5%] px-8 fixed z-30 top-0 items-center w-full justify-between py-4 bg-white dark:bg-secondary-black">
<header className="flex lg:w-[80%] lg:ml-[5%] px-8 fixed z-30 top-0 items-center w-full justify-between py-4 bg-white">
<div className="relative flex items-center justify-between w-full lg:hidden">
<FiMenu className="text-black w-6 h-6 mr-3" onClick={toggleSidebar} />
<div className="flex items-center gap-4">
Expand Down
Loading

0 comments on commit 21d101c

Please sign in to comment.