GoodTab.jsx 17 KB

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