picshop/src/components/forms/product.jsx
2024-12-10 11:55:52 +09:00

798 lines
34 KiB
JavaScript

'use client';
import * as Yup from 'yup';
import React from 'react';
import PropTypes from 'prop-types';
import toast from 'react-hot-toast';
import { capitalCase } from 'change-case';
import { useRouter } from 'next-nprogress-bar';
import { Form, FormikProvider, useFormik } from 'formik';
// mui
import { styled } from '@mui/material/styles';
import { LoadingButton } from '@mui/lab';
import {
Card,
Chip,
Grid,
Stack,
Select,
TextField,
Typography,
FormControl,
Autocomplete,
FormHelperText,
FormControlLabel,
FormGroup,
Skeleton,
Switch,
InputAdornment
} from '@mui/material';
// api
import * as api from 'src/services';
import { useMutation } from 'react-query';
import axios from 'axios';
// components
import UploadMultiFile from 'src/components/upload/UploadMultiFile';
import { fCurrency } from 'src/utils/formatNumber';
// ----------------------------------------------------------------------
const GENDER_OPTION = ['men', 'women', 'kids', 'others'];
const STATUS_OPTIONS = ['sale', 'new', 'regular', 'disabled'];
const LabelStyle = styled(Typography)(({ theme }) => ({
...theme.typography.subtitle2,
color: theme.palette.text.secondary,
lineHeight: 2.5
}));
// ----------------------------------------------------------------------
export default function ProductForm({
categories,
currentProduct,
categoryLoading = false,
isInitialized = false,
brands,
shops,
isVendor
}) {
const router = useRouter();
const [loading, setloading] = React.useState(false);
const { mutate, isLoading: updateLoading } = useMutation(
currentProduct ? 'update' : 'new',
currentProduct
? isVendor
? api.updateVendorProduct
: api.updateProductByAdmin
: isVendor
? api.createVendorProduct
: api.createProductByAdmin,
{
onSuccess: (data) => {
toast.success(data.message);
router.push((isVendor ? '/vendor' : '/admin') + '/products');
},
onError: (error) => {
toast.error(error.response.data.message);
}
}
);
const NewProductSchema = Yup.object().shape({
name: Yup.string().required('Product name is required'),
code: Yup.string().required('Product code is required'),
// tags: Yup.array().min(1, 'Tags is required'),
status: Yup.string().required('Status is required'),
// description: Yup.string().required('Description is required'),
category: Yup.string().required('Category is required'),
shop: isVendor ? Yup.string().nullable().notRequired() : Yup.string().required('Shop is required'),
subCategory: Yup.string().required('Sub Category is required'),
slug: Yup.string().required('Slug is required'),
// brand: Yup.string().required('brand is required'),
// metaTitle: Yup.string().required('Meta title is required'),
// metaDescription: Yup.string().required('Meta description is required'),
images: Yup.array().min(1, 'Images is required'),
// sku: Yup.string().required('Sku is required'),
available: Yup.number().required('Quantaty is required')
// colors: Yup.array().required('Color is required'),
// sizes: Yup.array().required('Size is required')
// price: Yup.number().required('Price is required'),
// priceSale: Yup.number()
// .required('Sale price is required')
// .lessThan(Yup.ref('price'), 'Sale price should be smaller than price')
});
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: currentProduct?.name || '',
description: currentProduct?.description || '',
code: currentProduct?.code || '',
slug: currentProduct?.slug || '',
metaTitle: currentProduct?.metaTitle || '',
metaDescription: currentProduct?.metaDescription || '',
brand: currentProduct?.brand || brands[0]?._id || 'brand',
tags: currentProduct?.tags || [],
gender: currentProduct?.gender || '',
category: currentProduct?.category || (categories.length && categories[0]?._id) || '',
shop: isVendor ? null : currentProduct?.shop || (shops?.length && shops[0]?._id) || '',
subCategory: currentProduct?.subCategory || (categories.length && categories[0].subCategories[0]?._id) || '',
status: currentProduct?.status || STATUS_OPTIONS[0],
blob: currentProduct?.blob || [],
isFeatured: currentProduct?.isFeatured || true,
sku: currentProduct?.sku || '',
price: currentProduct?.price || '',
priceSale: currentProduct?.priceSale || '',
colors: currentProduct?.colors || '',
sizes: currentProduct?.sizes || '',
available: currentProduct?.available || '',
images: currentProduct?.images || []
},
validationSchema: NewProductSchema,
onSubmit: async (values) => {
const { ...rest } = values;
try {
mutate({
...rest,
priceSale: values.priceSale || values.price,
...(currentProduct && { currentSlug: currentProduct.slug })
});
} catch (error) {
console.error(error);
}
}
});
const { errors, values, touched, handleSubmit, setFieldValue, getFieldProps } = formik;
const { mutate: deleteMutate } = useMutation(api.singleDeleteFile, {
onError: (error) => {
toast.error(error.response.data.message);
}
});
// handle drop
const handleDrop = (acceptedFiles) => {
setloading(true);
const uploaders = acceptedFiles.map((file) => {
const formData = new FormData();
formData.append('file', file);
formData.append('upload_preset', 'my-uploads');
setFieldValue('blob', values.blob.concat(acceptedFiles));
return axios.post(`https://api.cloudinary.com/v1_1/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload`, formData);
});
axios.all(uploaders).then((data) => {
const newImages = data.map(({ data }) => ({
url: data.secure_url,
_id: data.public_id
// blob: blobs[i],
}));
setloading(false);
setFieldValue('images', values.images.concat(newImages));
});
};
// handleAddVariants
// handleRemoveAll
const handleRemoveAll = () => {
values.images.forEach((image) => {
deleteMutate(image._id);
});
setFieldValue('images', []);
};
// handleRemove
const handleRemove = (file) => {
const removeImage = values.images.filter((_file) => {
if (_file._id === file._id) {
deleteMutate(file._id);
}
return _file !== file;
});
setFieldValue('images', removeImage);
};
const handleTitleChange = (event) => {
const title = event.target.value;
const slug = title
.toLowerCase()
// .replace(/[^a-zA-Z0-9\s]+/g, '')
.replace(/\s+/g, '-'); // convert to lowercase, remove special characters, and replace spaces with hyphens
formik.setFieldValue('slug', slug); // set the value of slug in the formik state
formik.handleChange(event); // handle the change in formik
};
return (
<Stack spacing={3}>
<FormikProvider value={formik}>
<Form noValidate autoComplete="off" onSubmit={handleSubmit}>
<Grid container spacing={3}>
<Grid item xs={12} md={7}>
<Stack spacing={3}>
<Card sx={{ p: 3 }}>
<Stack spacing={3}>
<div>
{isInitialized ? (
<Skeleton variant="text" width={140} />
) : (
<LabelStyle component={'label'} htmlFor="product-name">
{'Product Name'}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<TextField
id="product-name"
fullWidth
{...getFieldProps('name')}
onChange={handleTitleChange} // add onChange handler for title
error={Boolean(touched.name && errors.name)}
helperText={touched.name && errors.name}
/>
)}
</div>
<div>
<Grid container spacing={2}>
{isVendor ? null : (
<Grid item xs={12} md={6}>
<FormControl fullWidth>
{isInitialized ? (
<Skeleton variant="text" width={100} />
) : (
<LabelStyle component={'label'} htmlFor="shop-select">
{'Shop'}
</LabelStyle>
)}
<Select native {...getFieldProps('shop')} value={values.shop} id="shop-select">
{shops?.map((shop) => (
<option key={shop._id} value={shop._id}>
{shop.title}
</option>
))}
</Select>
{touched.shop && errors.shop && (
<FormHelperText error sx={{ px: 2, mx: 0 }}>
{touched.shop && errors.shop}
</FormHelperText>
)}
</FormControl>
</Grid>
)}
<Grid item xs={12} md={6}>
<FormControl fullWidth>
{isInitialized ? (
<Skeleton variant="text" width={100} />
) : (
<LabelStyle component={'label'} htmlFor="grouped-native-select">
{'Category'}
</LabelStyle>
)}
{!categoryLoading ? (
<Select
native
{...getFieldProps('category')}
value={values.category}
id="grouped-native-select"
>
{categories?.map((category) => (
<option key={category._id} value={category._id}>
{category.name}
</option>
// </optgroup>
))}
</Select>
) : (
<Skeleton variant="rectangular" width={'100%'} height={56} />
)}
{touched.category && errors.category && (
<FormHelperText error sx={{ px: 2, mx: 0 }}>
{touched.category && errors.category}
</FormHelperText>
)}
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
{isInitialized ? (
<Skeleton variant="text" width={100} />
) : (
<LabelStyle component={'label'} htmlFor="grouped-native-select-subCategory">
{'Sub Category'}
</LabelStyle>
)}
{!categoryLoading ? (
<Select
native
{...getFieldProps('subCategory')}
value={values.subCategory}
id="grouped-native-select-subCategory"
>
{categories
.find((v) => v._id.toString() === values.category)
?.subCategories?.map((subCategory) => (
<option key={subCategory._id} value={subCategory._id}>
{subCategory.name}
</option>
// </optgroup>
))}
</Select>
) : (
<Skeleton variant="rectangular" width={'100%'} height={56} />
)}
{touched.subCategory && errors.subCategory && (
<FormHelperText error sx={{ px: 2, mx: 0 }}>
{touched.subCategory && errors.subCategory}
</FormHelperText>
)}
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
{isInitialized ? (
<Skeleton variant="text" width={100} />
) : (
<LabelStyle component={'label'} htmlFor="brand-name">
{'Brand'}
</LabelStyle>
)}
<Select native {...getFieldProps('brand')} value={values.brand} id="grouped-native-select">
{brands?.map((brand) => (
<option key={brand._id} value={brand._id}>
{brand.name}
</option>
))}
</Select>
{touched.brand && errors.brand && (
<FormHelperText error sx={{ px: 2, mx: 0 }}>
{touched.brand && errors.brand}
</FormHelperText>
)}
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<LabelStyle component={'label'} htmlFor="size">
{'Sizes'}
</LabelStyle>
<Autocomplete
id="size"
multiple
freeSolo
value={values.sizes}
onChange={(event, newValue) => {
setFieldValue('sizes', newValue);
}}
options={[]}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip {...getTagProps({ index })} key={option} size="small" label={option} />
))
}
renderInput={(params) => (
<TextField
id=""
{...params}
error={Boolean(touched.sizes && errors.sizes)}
helperText={touched.sizes && errors.sizes}
/>
)}
/>
</Grid>
<Grid item xs={12} md={6}>
<LabelStyle component={'label'} htmlFor="color">
{'Colors'}
</LabelStyle>
<Autocomplete
id="color"
multiple
freeSolo
value={values.colors}
onChange={(event, newValue) => {
setFieldValue('colors', newValue);
}}
options={[]}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip {...getTagProps({ index })} key={option} size="small" label={option} />
))
}
renderInput={(params) => (
<TextField
id=""
{...params}
error={Boolean(touched.colors && errors.colors)}
helperText={touched.colors && errors.colors}
/>
)}
/>
</Grid>
<Grid item xs={12} md={6} sx={{display:'none'}}>
<FormControl fullWidth>
{isInitialized ? (
<Skeleton variant="text" width={80} />
) : (
<LabelStyle component={'label'} htmlFor="gander">
{'Gender'}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<Select
id="gander"
native
{...getFieldProps('gender')}
error={Boolean(touched.gender && errors.gender)}
>
<option value={''}>
<em>None</em>
</option>
{GENDER_OPTION.map((gender) => (
<option key={gender} value={gender}>
{capitalCase(gender)}
</option>
))}
</Select>
)}
</FormControl>
</Grid>
<Grid item xs={12} md={4} sx={{display:'none'}}>
<FormControl fullWidth>
{isInitialized ? (
<Skeleton variant="text" width={80} />
) : (
<LabelStyle component={'label'} htmlFor="status">
{'Status'}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<Select
id="status"
native
{...getFieldProps('status')}
error={Boolean(touched.status && errors.status)}
>
<option value="" style={{ display: 'none' }} />
{STATUS_OPTIONS.map((status) => (
<option key={status} value={status}>
{capitalCase(status)}
</option>
))}
</Select>
)}
{touched.status && errors.status && (
<FormHelperText error sx={{ px: 2, mx: 0 }}>
{touched.status && errors.status}
</FormHelperText>
)}
</FormControl>
</Grid>
<Grid item xs={12} md={4}>
<div>
{isInitialized ? (
<Skeleton variant="text" width={120} />
) : (
<LabelStyle component={'label'} htmlFor="product-code">
{'Product Code'}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<TextField
id="product-code"
fullWidth
{...getFieldProps('code')}
error={Boolean(touched.code && errors.code)}
helperText={touched.code && errors.code}
/>
)}
</div>
</Grid>
<Grid item xs={12} md={4}>
<div>
<LabelStyle component={'label'} htmlFor="product-sku">
{'Product Sku'}
</LabelStyle>
<TextField
id="product-sku"
fullWidth
{...getFieldProps('sku')}
error={Boolean(touched.sku && errors.sku)}
helperText={touched.sku && errors.sku}
/>
</div>
</Grid>
<Grid item xs={12} md={12}>
{isInitialized ? (
<Skeleton variant="text" width={70} />
) : (
<LabelStyle component={'label'} htmlFor="tags">
{'Tags'}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<Autocomplete
id="tags"
multiple
freeSolo
value={values.tags}
onChange={(event, newValue) => {
setFieldValue('tags', newValue);
}}
options={[]}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip {...getTagProps({ index })} key={option} size="small" label={option} />
))
}
renderInput={(params) => (
<TextField
id=""
{...params}
error={Boolean(touched.tags && errors.tags)}
helperText={touched.tags && errors.tags}
/>
)}
/>
)}
</Grid>
<Grid item xs={12} md={12}>
<div>
{isInitialized ? (
<Skeleton variant="text" width={100} />
) : (
<LabelStyle component={'label'} htmlFor="meta-title">
{'Meta Title'}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<TextField
id="meta-title"
fullWidth
{...getFieldProps('metaTitle')}
error={Boolean(touched.metaTitle && errors.metaTitle)}
helperText={touched.metaTitle && errors.metaTitle}
/>
)}
</div>
</Grid>
<Grid item xs={12} md={12}>
<div>
{isInitialized ? (
<Skeleton variant="text" width={120} />
) : (
<LabelStyle component={'label'} htmlFor="description">
{'Description'}{' '}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={240} />
) : (
<TextField
id="description"
fullWidth
{...getFieldProps('description')}
error={Boolean(touched.description && errors.description)}
helperText={touched.description && errors.description}
rows={9}
multiline
/>
)}
</div>
</Grid>
<Grid item xs={12} md={12}>
<div>
<LabelStyle component={'label'} htmlFor="product-image">
{'Products Images'} <span>1080 * 1080</span>
</LabelStyle>
<UploadMultiFile
id="product-image"
showPreview
maxSize={3145728}
accept="image/*"
files={values?.images}
loading={loading}
onDrop={handleDrop}
onRemove={handleRemove}
onRemoveAll={handleRemoveAll}
blob={values.blob}
error={Boolean(touched.images && errors.images)}
/>
{touched.images && errors.images && (
<FormHelperText error sx={{ px: 2 }}>
{touched.images && errors.images}
</FormHelperText>
)}
</div>
</Grid>
</Grid>
</div>
</Stack>
</Card>
</Stack>
</Grid>
<Grid item xs={12} md={5}>
<Card sx={{ p: 3 }}>
<Stack spacing={3} pb={1}>
<div>
{isInitialized ? (
<Skeleton variant="text" width={70} />
) : (
<LabelStyle component={'label'} htmlFor="slug">
{'Slug'}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<TextField
id="slug"
fullWidth
{...getFieldProps('slug')}
error={Boolean(touched.slug && errors.slug)}
helperText={touched.slug && errors.slug}
/>
)}
</div>
<div>
{isInitialized ? (
<Skeleton variant="text" width={140} />
) : (
<LabelStyle component={'label'} htmlFor="meta-description">
{'Meta Description'}{' '}
</LabelStyle>
)}
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={240} />
) : (
<TextField
id="meta-description"
fullWidth
{...getFieldProps('metaDescription')}
error={Boolean(touched.metaDescription && errors.metaDescription)}
helperText={touched.metaDescription && errors.metaDescription}
rows={9}
multiline
/>
)}
</div>
<div style={{display:'none'}}>
<LabelStyle component={'label'} htmlFor="quantity">
{'Quantity'}
</LabelStyle>
<TextField
id="quantity"
fullWidth
type="number"
{...getFieldProps('available')}
error={Boolean(touched.available && errors.available)}
helperText={touched.available && errors.available}
/>
</div>
<div style={{display:'none'}}>
<LabelStyle component={'label'} htmlFor="regular-price">
{'Regular Price'}
</LabelStyle>
<TextField
id="regular-price"
fullWidth
placeholder="0.00"
{...getFieldProps('price')}
InputProps={{
startAdornment: <InputAdornment position="start">{fCurrency(0)?.split('0')[0]}</InputAdornment>,
type: 'number'
}}
error={Boolean(touched.price && errors.price)}
helperText={touched.price && errors.price}
/>
</div>
<div style={{display:'none'}}>
<LabelStyle component={'label'} htmlFor="sale-price">
{'Sale Price'}
</LabelStyle>
<TextField
id="sale-price"
fullWidth
placeholder="0.00"
{...getFieldProps('priceSale')}
InputProps={{
startAdornment: <InputAdornment position="start">{fCurrency(0)?.split('0')[0]}</InputAdornment>,
type: 'number'
}}
error={Boolean(touched.priceSale && errors.priceSale)}
helperText={touched.priceSale && errors.priceSale}
/>
</div>
<div>
<FormGroup>
<FormControlLabel
control={
<Switch
onChange={(e) => setFieldValue('isFeatured', e.target.checked)}
checked={values.isFeatured}
/>
}
label={'Featured Product'}
/>
</FormGroup>
</div>
<Stack spacing={2}>
{isInitialized ? (
<Skeleton variant="rectangular" width="100%" height={56} />
) : (
<LoadingButton type="submit" variant="contained" size="large" fullWidth loading={updateLoading}>
{currentProduct ? 'Update Product' : 'Create Product'}
</LoadingButton>
)}
</Stack>
</Stack>
</Card>
</Grid>
</Grid>
</Form>
</FormikProvider>
</Stack>
);
}
ProductForm.propTypes = {
categories: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
subCategories: PropTypes.array.isRequired
// ... add other required properties for category
})
).isRequired,
currentProduct: PropTypes.shape({
_id: PropTypes.string,
name: PropTypes.string,
description: PropTypes.string,
code: PropTypes.string,
slug: PropTypes.string,
metaTitle: PropTypes.string,
metaDescription: PropTypes.string,
brand: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.string),
gender: PropTypes.string,
category: PropTypes.string,
subCategory: PropTypes.string,
status: PropTypes.string,
blob: PropTypes.array,
isFeatured: PropTypes.bool,
sku: PropTypes.string,
price: PropTypes.number,
priceSale: PropTypes.number,
colors: PropTypes.arrayOf(PropTypes.string),
sizes: PropTypes.arrayOf(PropTypes.string),
available: PropTypes.number,
images: PropTypes.array
// ... add other optional properties for currentProduct
}),
categoryLoading: PropTypes.bool,
isInitialized: PropTypes.bool,
isVendor: PropTypes.bool,
brands: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
// ... add other required properties for brands
})
)
};