GoodTab.jsx 17 KB

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