GoodTab.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import {useEffect, useState} from "react";
  2. import {useDropzone} from "react-dropzone";
  3. import {sortableContainer, sortableElement} from "react-sortable-hoc";
  4. import {arrayMoveImmutable} from "array-move";
  5. import Box from "@mui/material/Box";
  6. import Typography from "@mui/material/Typography";
  7. import {
  8. Button,
  9. CircularProgress, Container,
  10. FormControl,
  11. Grid, IconButton,
  12. InputAdornment,
  13. InputLabel,
  14. MenuItem,
  15. Select,
  16. TextField
  17. } from "@mui/material";
  18. import {connect} from "react-redux";
  19. import {actionFullRootCats} from "../../actions/ActionCategory";
  20. import {actionGoodUpsert} from "../../actions/ActionCreateGood";
  21. import Autocomplete from "@mui/material/Autocomplete";
  22. import {actionFullGoodFind, actionGoodCount} from "../../actions/ActionGoodFind";
  23. import {actionUploadFile} from "../../actions/ActionUploadFile";
  24. import {backURL} from "../../actions/PathDB";
  25. import {actionClearPromise} from "../../reducers/PromiseReducer";
  26. import {Link} from "react-router-dom";
  27. import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
  28. import SearchIcon from "@material-ui/icons/Search";
  29. import imgNotFound from "../../img/catalog/imgNotFound.png";
  30. import {actionSearchRemove} from "../../reducers/SearchReducer";
  31. const GoodEdit = ({entity={images: [], categories: []}, onSave, onFileDrop, fileStatus, variant='create', categoryState, actionRootCat, goodCount, goods, actionClear, result}) => {
  32. const [state, setState] = useState(entity)
  33. const {getRootProps, getInputProps, isDragActive} = useDropzone({accept: 'image/*', onDrop: acceptedFiles => {
  34. acceptedFiles.forEach(async file => {
  35. await onFileDrop(file)
  36. })
  37. }})
  38. const SortableItem = sortableElement(({value}) => {
  39. return <div key={value?._id} style={{display: 'inline-flex', borderRadius: 2,border: '1px solid #eaeaea',marginBottom: 8, marginRight: 8, width: 200, height: 200, padding: 4, boxSizing: 'border-box'}}>
  40. <div style={{display: 'flex', justifyContent: 'center', minWidth: 0, overflow: 'hidden'}}>
  41. {value?.url ?
  42. <img src={backURL+ '/' + value?.url} style={{display: 'block', width: 'auto', height: '100%', objectFit: 'cover', objectPosition: 'center center'}} alt={value.name}/>
  43. :
  44. <Box sx={{ display: 'flex' }}>
  45. <CircularProgress />
  46. </Box>
  47. }
  48. </div>
  49. </div>
  50. });
  51. const SortableContainer = sortableContainer(({children}) => {
  52. return <aside style={{display:'flex', justifyContent: 'space-between', flexWrap: 'wrap'}}>
  53. {children}
  54. </aside>
  55. })
  56. const onSortEnd = ({oldIndex, newIndex}) => {
  57. setState(({images}) => ({
  58. ...state,
  59. images: arrayMoveImmutable(images, oldIndex, newIndex),
  60. }));
  61. }
  62. const handleClear = () => {
  63. setState(entity)
  64. }
  65. const handleOnSave = () => {
  66. let query = {...state}
  67. state.images.length > 0 ? query.images = state.images.map(item => {return {'_id': item['_id']}}) : delete query.images
  68. state.categories.length > 0 ? query.categories = state.categories.map(item => {return {'_id': item['_id'], 'name': item['name']}}) : delete query.categories
  69. onSave(query)
  70. goodCount()
  71. }
  72. const handleFullClear = () => {
  73. setState(entity)
  74. actionClear('goodUpsert')
  75. actionClear('uploadFile')
  76. }
  77. useEffect(() => {
  78. if(!categoryState || Object.entries(categoryState).length === 0) actionRootCat()
  79. if(!goods) goodCount()
  80. if(fileStatus?.status === 'RESOLVED'){
  81. setState({...state, images: [...state.images, fileStatus?.payload]})
  82. }
  83. },[categoryState, goods, fileStatus])
  84. return (
  85. <>
  86. {!result ?
  87. <>
  88. <Typography variant='h6' letterSpacing='2px' marginBottom='20px'>Total products: {goods?.payload || 0}</Typography>
  89. <Box style={{minHeight: "200px", border: '1px dashed #616161', borderRadius: '20px', padding: '20px'}} {...getRootProps()}>
  90. <input {...getInputProps()} />
  91. {isDragActive ?
  92. <Typography variant='body1' textAlign='center' color='#616161'>Drop the file here ...</Typography> :
  93. <Typography variant='body1' textAlign='center' color='#616161' marginBottom='20px'>Drag 'n' drop image files here, or click to select file</Typography>
  94. }
  95. <SortableContainer axis="xy" onSortEnd={onSortEnd}>
  96. {state.images.length > 0 && state.images.map((value, index) => (
  97. <SortableItem key={`item-${value?._id}`} index={index} value={value} />
  98. ))}
  99. </SortableContainer>
  100. </Box>
  101. <Grid container justifyContent='space-between' marginTop='30px'>
  102. <Grid item xs={5.5}>
  103. <TextField fullWidth id="filled-basic" label="Title product" variant="standard" value={state?.name || ''} onChange={e => setState({...state, name: e.target.value})}/>
  104. </Grid>
  105. <Grid item xs={5.5}>
  106. {categoryState &&
  107. <Autocomplete
  108. multiple
  109. id="tags-standard"
  110. options={Object.values(categoryState)}
  111. onChange={(event, newValue) => {
  112. setState({...state, categories: [...newValue]})
  113. }}
  114. getOptionLabel={(option) => state?.categories ? [...state.categories] : option?.name || 'no name'}
  115. key={option => option?.id}
  116. renderInput={(params) => (
  117. <TextField
  118. {...params}
  119. variant="standard"
  120. label="Select categories"
  121. placeholder="categories"
  122. />
  123. )}
  124. />
  125. }
  126. </Grid>
  127. </Grid>
  128. <Grid container justifyContent='space-between' marginTop='30px'>
  129. <Grid item xs={5.5}>
  130. <TextField fullWidth
  131. id='Price'
  132. type='number'
  133. label='Price'
  134. variant='standard'
  135. value={state?.price || ''}
  136. onChange={e => setState({...state, price: parseFloat(e.target.value < 0 ? 0 : e.target.value)})}
  137. />
  138. </Grid>
  139. <Grid item xs={5.5}>
  140. <TextField fullWidth
  141. id='filled-basic'
  142. label='Description product'
  143. variant='standard'
  144. multiline
  145. value={state?.description || ''}
  146. onChange={e => setState({...state, description: e.target.value})}
  147. />
  148. </Grid>
  149. </Grid>
  150. <Grid container justifyContent='space-between' marginTop='30px'>
  151. <Grid item xs={5.5} display='flex' justifyContent='center'>
  152. <Button
  153. fullWidth
  154. onClick={handleClear}
  155. variant="outlined"
  156. color='warning'
  157. >
  158. Clear
  159. </Button>
  160. </Grid>
  161. <Grid item xs={5.5} display='flex' justifyContent='center'>
  162. <Button
  163. fullWidth
  164. variant="outlined"
  165. color='primary'
  166. onClick={handleOnSave}
  167. >
  168. Save
  169. </Button>
  170. </Grid>
  171. </Grid>
  172. </> :
  173. result?.payload?._id ?
  174. <>
  175. <Box display='flex' alignItems='center' flexDirection='column'>
  176. <Typography variant='h5' letterSpacing='2px' textAlign='center' color='#616161' marginBottom='20px'>Product successfully created!</Typography>
  177. <CheckCircleOutlineIcon sx={{marginBottom: '20px'}}/>
  178. <Link to={`/good/${result?.payload?._id}`} style={{color:'#616161', marginBottom:'20px'}}>
  179. <Typography variant='h5' letterSpacing='2px' textAlign='center' color='#616161'>View results</Typography>
  180. </Link>
  181. <Button variant='outlined' onClick={handleFullClear}>Add more</Button>
  182. </Box>
  183. </> :
  184. result?.error ?
  185. <Box display='flex' alignItems='center' flexDirection='column'>
  186. <Typography variant='h5' letterSpacing='2px' textAlign='center' color='#f00' marginBottom='20px'>Fatal error, try again!</Typography>
  187. <Button variant='outlined' onClick={handleFullClear}>Add more</Button>
  188. </Box>
  189. :
  190. <Box sx={{ display: 'flex' }}>
  191. <CircularProgress />
  192. </Box>
  193. }
  194. </>
  195. )
  196. }
  197. export const CGoodEdit = connect(state => ({fileStatus: state.promise['uploadFile'], categoryState: state.category, goods: state.promise['goodCount'], result: state.promise['goodUpsert']}), {actionRootCat: actionFullRootCats, onSave: actionGoodUpsert, goodCount: actionGoodCount, onFileDrop: actionUploadFile, actionClear: actionClearPromise})(GoodEdit)
  198. const ItemFound = ({item:{_id, name, price, images, description, categories}}) => {
  199. let [state, setState] = useState(false)
  200. return (
  201. !state ?
  202. <Button style={{textDecoration: 'none', display: 'flex', alignItems: 'center', marginBottom: '30px'}} onClick={() => setState(true)}>
  203. <Box width='60px' height='60px' borderRadius='10px' overflow='hidden' marginRight='60px'
  204. position='relative'>
  205. <img style={{
  206. position: 'absolute',
  207. top: '0',
  208. left: '0',
  209. width: '100%',
  210. height: '100%',
  211. objectFit: 'cover'
  212. }} src={images && Array.isArray(images) && images[0]?.url ? backURL + '/' + images[0].url : imgNotFound}
  213. alt={name}/>
  214. </Box>
  215. <Box sx={{
  216. display: 'flex',
  217. flexDirection: 'column',
  218. justifyContent: 'space-between',
  219. alignItems: 'flex-start'
  220. }}>
  221. <Typography
  222. color='#000'
  223. letterSpacing='1px'
  224. fontFamily='sarif'
  225. fontWeight='600'
  226. variant='h6'
  227. >
  228. {name}
  229. </Typography>
  230. <Typography
  231. letterSpacing='1px'
  232. variant='body1'
  233. fontWeight='300'
  234. color='#616161'
  235. margin='10px 0'
  236. >
  237. {description?.length > 60 ? 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.' : description}
  238. </Typography>
  239. <Typography
  240. color='#000'
  241. letterSpacing='1px'
  242. variant='body1'
  243. fontWeight='600'
  244. >
  245. ${parseFloat(price).toFixed(2)}
  246. </Typography>
  247. </Box>
  248. </Button>
  249. :
  250. <CGoodEdit entity={{_id, name, price, images, description, categories}}/>
  251. )
  252. }
  253. const NotFound = () => {
  254. return (
  255. <Typography
  256. textAlign='center'
  257. color='#000'
  258. letterSpacing='1px'
  259. variant='body1'
  260. >
  261. No results found
  262. </Typography>
  263. )
  264. }
  265. const FindGoodEdit = ({searchResult, onSearch, onSearchRemove}) => {
  266. const [value, setValue] = useState('')
  267. const [click, setClick] = useState(false)
  268. return (
  269. <>
  270. <Container maxWidth="sm">
  271. <Typography
  272. variant='h5'
  273. fontFamily='sarif'
  274. letterSpacing='3px'
  275. marginBottom='30px'
  276. marginTop='30px'
  277. textAlign='center'
  278. >
  279. WHICH ITEM TO EDIT?
  280. </Typography>
  281. <TextField
  282. color={'primary'}
  283. fullWidth
  284. variant="standard"
  285. value={value}
  286. placeholder="Start typing..."
  287. onChange={(event) => {setClick(false); setValue(event.target.value); onSearchRemove()}}
  288. InputProps={{
  289. sx: {padding: '10px', outline:'none', color: '#616161', fontWeight: '300', letterSpacing: '1px', marginBottom: '50px'},
  290. endAdornment: (
  291. <InputAdornment position="end">
  292. <IconButton onClick={() => {setClick(true); onSearchRemove(); onSearch(value)}}>
  293. <SearchIcon />
  294. </IconButton>
  295. </InputAdornment>
  296. )
  297. }}
  298. />
  299. {(value !== '' && click) && (searchResult?.searchResult ?
  300. Object.values(searchResult.searchResult).length > 0 ?
  301. Object.values(searchResult.searchResult).map(item => <ItemFound item={item}/>) : <NotFound/> :
  302. <CircularProgress color="inherit"/>
  303. )}
  304. </Container>
  305. </>
  306. )
  307. }
  308. export const CFindGoodEdit = connect(state=>({searchResult: state.search}), {onSearch: actionFullGoodFind, onSearchRemove: actionSearchRemove})(FindGoodEdit)