atlp-rwanda/hackers-ec-Fe

View on GitHub
src/components/Forms/AddProductForm.tsx

Summary

Maintainability
D
2 days
Test Coverage
B
86%
import { zodResolver } from '@hookform/resolvers/zod';
import { X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useLocation, useNavigate } from 'react-router-dom';
import { HashLoader } from 'react-spinners';
import { DynamicData } from '../../@types/DynamicData';
import useToast from '../../hooks/useToast';
import { fetchCategories } from '../../redux/features/categorySlice';
import { addProduct, updateProduct } from '../../redux/features/productSlice';
import { useAppDispatch, useAppSelector } from '../../redux/hooks/hooks';
import {
    productTypes,
    productValidation,
} from '../../validations/products/addProductValidation';
import IconLoader from '../Loaders/IconLoader';
import Button from '../buttons/Button';
import ImageDropZone from '../cards/ImageDropZone';
import FormInput from './InputText';

const AddEditProductForm = () => {
    const location = useLocation();
    const navigate = useNavigate();
    const dispatch = useAppDispatch();
    const { showErrorMessage, showSuccessMessage } = useToast();
    const { isLoading, categories } = useAppSelector((state) => state.categories);
    const { isLoading: processing } = useAppSelector((state) => state.product);

    const defaultProductData: productTypes = {
        name: '',
        price: '',
        quantity: '',
        discount: '',
        expiryDate: '',
        images: [],
        categoryId: '',
    };

    const data = location.state || defaultProductData;

    const productData: productTypes = {
        ...data,
        price: String(data.price),
        quantity: String(data.quantity),
        discount: String(data.discount),
        expiryDate: String(data.expiryDate).split('T')[0],
    };

    const [images, setImages] = useState<string[]>(productData?.images || []);
    const [imageFiles, setImageFiles] = useState<File[]>([]);

    useEffect(() => {
        dispatch(fetchCategories());
    }, [dispatch]);

    const {
        register,
        handleSubmit,
        formState: { errors },
        setValue,
    } = useForm<productTypes>({
        resolver: zodResolver(productValidation),
        defaultValues: productData || defaultProductData,
    });

    const handleDrop = (files: FileList) => {
        const uploadedImages = Array.from(files).map((file) =>
            URL.createObjectURL(file),
        );
        setImageFiles((prev) => [...prev, ...Array.from(files)]);
        setImages((prevImages) => [...prevImages, ...uploadedImages]);
    };

    const handleImages = (e: DynamicData) => {
        const files = Array.from(e.target.files) as File[];
        const uploadedImages = files.map((file) =>
            URL.createObjectURL(file as unknown as MediaSource),
        );
        setImageFiles((prev) => [...prev, ...files]);
        setImages((prevImages) => [...prevImages, ...uploadedImages]);
    };

    const removeImage = (index: number) => {
        setImageFiles((prev) => prev.filter((_, i) => i !== index));
        setImages((prevImages) => prevImages.filter((_, i) => i !== index));
    };

    useEffect(() => {
        setValue('images', imageFiles);
    }, [imageFiles, setValue]);

    const onSubmit: SubmitHandler<productTypes> = async (
        formData: productTypes,
    ) => {
        try {
            if (productData.id) {
                const res = await dispatch(
                    updateProduct({ productData: formData, id: productData.id }),
                ).unwrap();
                showSuccessMessage(res.message);
            } else {
                const res = await dispatch(addProduct(formData)).unwrap();
                showSuccessMessage(res.message);
            }
            navigate('/dashboard/products');
        } catch (e) {
            const err = e as DynamicData;
            showErrorMessage(
                err?.data?.message ||
                    err?.message ||
                    'Unknown error occurred! Please try again!',
            );
        }
    };

    if (isLoading) {
        return (
            <div className="flex-1 h-full flex-center flex-col gap-4">
                <HashLoader color="#266491" size={60} role="progressbar" />
                <p className="text-xs">Please wait ...</p>
            </div>
        );
    }

    return (
        <form
            onSubmit={handleSubmit(onSubmit)}
            className="flex-1 items-start flex-wrap gap-4 text-xs h-[85%] overflow-y-scroll grid grid-cols-1 ipad:grid-cols-2 pb-10 ipad:p-5"
            data-testid="form"
        >
            <div className="bg-neutral-white flex-1 p-5 flex gap-7 h-full flex-col rounded-xl">
                <div>
                    <label htmlFor="" className="labels">
                        Product Name:
                    </label>
                    <FormInput
                        placeholder="Your product name"
                        data-testid="input"
                        type="text"
                        otherStyles="p-2 rounded-md mt-2"
                        {...register('name')}
                        error={errors.name}
                    />
                </div>
                <div>
                    <label htmlFor="" className="labels">
                        Product price:
                    </label>
                    <FormInput
                        placeholder="Your product price"
                        {...register('price')}
                        type="number"
                        otherStyles="p-2 rounded-md mt-2"
                        error={errors.price}
                    />
                </div>
                <div>
                    <label htmlFor="" className="labels">
                        Product discount:
                    </label>
                    <FormInput
                        placeholder="Your product discount"
                        {...register('discount')}
                        type="number"
                        otherStyles="p-2 rounded-md mt-2"
                        error={errors.discount}
                    />
                </div>
                <div>
                    <label htmlFor="" className="labels">
                        Product quantity:
                    </label>
                    <FormInput
                        placeholder="Quantity available"
                        {...register('quantity')}
                        type="number"
                        otherStyles="p-2 rounded-md mt-2"
                        error={errors.quantity}
                    />
                </div>
                <div>
                    <label htmlFor="" className="labels">
                        Product category:
                    </label>
                    <div className="w-full bg-[#D9D9D9] text-black/75 p-2 rounded-md mt-2">
                        <select
                            {...register('categoryId')}
                            className="w-full outline-none bg-[#D9D9D9]"
                            aria-label="select-category"
                        >
                            <option value={''} disabled>
                                Select product category
                            </option>
                            {categories &&
                                categories.map((category) => (
                                    <option key={category.id} value={category.id}>
                                        {category.name}
                                    </option>
                                ))}
                        </select>
                    </div>
                    {errors.categoryId?.message && (
                        <p className="text-[9px] text-action-error text-end px-2">
                            {errors.categoryId.message}
                        </p>
                    )}
                </div>
                <div>
                    <label htmlFor="" className="labels">
                        Product expiry date:
                    </label>
                    <FormInput
                        placeholder="2525-12-03"
                        {...register('expiryDate')}
                        type="date"
                        otherStyles="p-2 rounded-md mt-2"
                        error={errors.expiryDate}
                    />
                </div>
            </div>
            <div className="relative bg-neutral-white p-5 h-max ipad:h-full rounded-xl">
                <div className="h-[40px] mb-3 rounded-xl flex items-center px-4 bg-neutral-black/15">
                    Product images
                </div>
                <ImageDropZone onDrop={handleDrop} handleOnChange={handleImages} />
                {errors.images?.message && (
                    <p className="text-[9px] text-action-error text-end px-2">
                        {errors.images.message}
                    </p>
                )}
                <div className="relative grid grid-cols-1 mobile:grid-cols-2 gap-4 min-h-[300px] ipad:h-[60%] overflow-y-scroll py-4">
                    {images.map((src, index) => (
                        <div
                            key={src}
                            className="relative h-40 border border-dashed border-neutral-grey p-3 rounded-2xl"
                        >
                            <div
                                onClick={() => removeImage(index)}
                                className="absolute top-2 right-2 bg-action-error p-1.5 text-neutral-white shadow-inner rounded-full"
                            >
                                <X size={18} />
                            </div>
                            <img
                                src={src}
                                alt={`Preview ${index}`}
                                className="w-full h-full object-contain"
                            />
                        </div>
                    ))}
                </div>
                <div className="h-[10%] grid grid-cols-1 ipad:grid-cols-2 gap-3 items-center">
                    <Button
                        buttonType="submit"
                        url={null}
                        title={
                            processing ? (
                                <>
                                    <IconLoader className="animate-spin mr-1" />{' '}
                                    {'processing....'}
                                </>
                            ) : productData.id ? (
                                'Update'
                            ) : (
                                'Save'
                            )
                        }
                        color="bg-action-success"
                        otherStyles="rounded-md h-10"
                        disabled={processing}
                    />
                    <Button
                        title="Cancel"
                        buttonType="button"
                        url={'/dashboard/products'}
                        color="bg-action-error"
                        otherStyles="rounded-md h-10"
                    />
                </div>
            </div>
        </form>
    );
};

export default AddEditProductForm;