diff options
| author | Dayana31 <[email protected]> | 2022-04-21 17:27:08 -0500 |
|---|---|---|
| committer | Dayana31 <[email protected]> | 2022-04-21 17:27:08 -0500 |
| commit | 67c50667678dd0ce4709b29a854f6a47093a1ac5 (patch) | |
| tree | b6f9f39092ad54bf6b815984d32b37d7c7ca67ab /front/odiparpack/app/components | |
| parent | 91140b24f0d49a9f89a080ee063e9eb023a4b73a (diff) | |
| parent | e13e630cd6e4fc0b1ff92098a28a770794c7bb9a (diff) | |
| download | DP1_project-67c50667678dd0ce4709b29a854f6a47093a1ac5.tar.gz DP1_project-67c50667678dd0ce4709b29a854f6a47093a1ac5.tar.bz2 DP1_project-67c50667678dd0ce4709b29a854f6a47093a1ac5.zip | |
Merge branch 'gabshr' into dayana
Diffstat (limited to 'front/odiparpack/app/components')
126 files changed, 15358 insertions, 0 deletions
diff --git a/front/odiparpack/app/components/.DS_Store b/front/odiparpack/app/components/.DS_Store Binary files differnew file mode 100644 index 0000000..a82440f --- /dev/null +++ b/front/odiparpack/app/components/.DS_Store diff --git a/front/odiparpack/app/components/Badges/LimitedBadges.js b/front/odiparpack/app/components/Badges/LimitedBadges.js new file mode 100644 index 0000000..a0e0ecd --- /dev/null +++ b/front/odiparpack/app/components/Badges/LimitedBadges.js @@ -0,0 +1,27 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Badge } from '@material-ui/core'; + +class LimitedBadges extends PureComponent { + render() { + const { + children, + limit, + value, + ...rest + } = this.props; + return ( + <Badge badgeContent={value > limit ? limit + '+' : value} {...rest}> + { children } + </Badge> + ); + } +} + +LimitedBadges.propTypes = { + children: PropTypes.node.isRequired, + value: PropTypes.number.isRequired, + limit: PropTypes.number.isRequired, +}; + +export default LimitedBadges; diff --git a/front/odiparpack/app/components/BreadCrumb/BreadCrumb.js b/front/odiparpack/app/components/BreadCrumb/BreadCrumb.js new file mode 100644 index 0000000..0d11753 --- /dev/null +++ b/front/odiparpack/app/components/BreadCrumb/BreadCrumb.js @@ -0,0 +1,55 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { Link, Route } from 'react-router-dom'; +import styles from './breadCrumb-jss'; + +const Breadcrumbs = (props) => { + const { + classes, + theme, + separator, + location + } = props; + return ( + <section className={classNames(theme === 'dark' ? classes.dark : classes.light, classes.breadcrumbs)}> + <Route + path="*" + render={() => { + let parts = location.pathname.split('/'); + const place = parts[parts.length - 1]; + parts = parts.slice(1, parts.length - 1); + return ( + <p> + You are here: + <span> + { + parts.map((part, partIndex) => { + const path = ['', ...parts.slice(0, partIndex + 1)].join('/'); + return ( + <Fragment key={path}> + <Link to={path}>{part}</Link> + { separator } + </Fragment> + ); + }) + } + {place} + </span> + </p> + ); + }} + /> + </section> + ); +}; + +Breadcrumbs.propTypes = { + classes: PropTypes.object.isRequired, + location: PropTypes.object.isRequired, + theme: PropTypes.string.isRequired, + separator: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(Breadcrumbs); diff --git a/front/odiparpack/app/components/BreadCrumb/breadCrumb-jss.js b/front/odiparpack/app/components/BreadCrumb/breadCrumb-jss.js new file mode 100644 index 0000000..fe2dc47 --- /dev/null +++ b/front/odiparpack/app/components/BreadCrumb/breadCrumb-jss.js @@ -0,0 +1,29 @@ +const styles = theme => ({ + dark: {}, + breadcrumbs: { + position: 'relative', + display: 'block', + fontSize: 12, + color: 'rgba(255, 255, 255, 0.5)', + '& p': { + display: 'block', + '& span': { + textTransform: 'capitalize', + marginLeft: 5, + }, + '& a': { + color: theme.palette.common.white, + textDecoration: 'none', + margin: '0 5px' + } + }, + '&$dark': { + color: theme.palette.grey[900], + '& a': { + color: theme.palette.grey[900] + } + } + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Calendar/AddEvent.js b/front/odiparpack/app/components/Calendar/AddEvent.js new file mode 100644 index 0000000..ef1f5a5 --- /dev/null +++ b/front/odiparpack/app/components/Calendar/AddEvent.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Add from '@material-ui/icons/Add'; +import { Fab, Tooltip } from '@material-ui/core'; +import FloatingPanel from '../Panel/FloatingPanel'; +import AddEventForm from './AddEventForm'; +import styles from './calendar-jss.js'; + + +class AddEvent extends React.Component { + showResult(values) { + setTimeout(() => { + this.props.submit(values); + }, 500); // simulate server latency + } + + render() { + const { + classes, + openForm, + closeForm, + addEvent + } = this.props; + const branch = ''; + return ( + <div> + <Tooltip title="Add New Event"> + <Fab color="secondary" onClick={() => addEvent()} className={classes.addBtn}> + <Add /> + </Fab> + </Tooltip> + <FloatingPanel title="Add New Event" openForm={openForm} branch={branch} closeForm={() => closeForm()}> + <AddEventForm onSubmit={(values) => this.showResult(values)} /> + </FloatingPanel> + </div> + ); + } +} + +AddEvent.propTypes = { + classes: PropTypes.object.isRequired, + openForm: PropTypes.bool.isRequired, + addEvent: PropTypes.func.isRequired, + closeForm: PropTypes.func.isRequired, + submit: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(AddEvent); diff --git a/front/odiparpack/app/components/Calendar/AddEventForm.js b/front/odiparpack/app/components/Calendar/AddEventForm.js new file mode 100644 index 0000000..9edccca --- /dev/null +++ b/front/odiparpack/app/components/Calendar/AddEventForm.js @@ -0,0 +1,178 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { KeyboardDatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import MomentUtils from '@date-io/moment'; +import { reduxForm, Field } from 'redux-form/immutable'; +import { connect } from 'react-redux'; +import css from 'ba-styles/Form.scss'; +import { Button, Radio, RadioGroup, FormLabel, FormControlLabel } from '@material-ui/core'; +import { TextFieldRedux } from '../Forms/ReduxFormMUI'; +import styles from './calendar-jss'; + + +// validation functions +const required = value => (value == null ? 'Required' : undefined); + +const DateTimePickerRow = props => { + const { + showErrorsInline, + dispatch, + input: { onChange, value }, + meta: { touched, error }, + ...other + } = props; + + const showError = showErrorsInline || touched; + return ( + <MuiPickersUtilsProvider utils={MomentUtils}> + <KeyboardDatePicker + error={!!(showError && error)} + helperText={showError && error} + value={value || new Date()} + onChange={onChange} + disablePast + label="DateTimePicker" + {...other} + /> + </MuiPickersUtilsProvider> + ); +}; + +DateTimePickerRow.propTypes = { + showErrorsInline: PropTypes.bool, + dispatch: PropTypes.func, + input: PropTypes.object.isRequired, + meta: PropTypes.object.isRequired, +}; + +const renderRadioGroup = ({ input, ...rest }) => ( + <RadioGroup + {...input} + {...rest} + valueselected={input.value} + onChange={(event, value) => input.onChange(value)} + /> +); + +renderRadioGroup.propTypes = { + input: PropTypes.object.isRequired, +}; + +DateTimePickerRow.defaultProps = { + showErrorsInline: false, + dispatch: () => {}, +}; + +class AddEventForm extends React.Component { + state = { + selectedDate: new Date(), + } + + onChangeDate = date => { + this.setState({ selectedDate: date }); + } + + saveRef = ref => { + this.ref = ref; + return this.ref; + }; + + render() { + const { + classes, + reset, + pristine, + submitting, + handleSubmit, + } = this.props; + const { selectedDate } = this.state; + return ( + <div> + <form onSubmit={handleSubmit}> + <section className={css.bodyForm}> + <div> + <Field + name="title" + component={TextFieldRedux} + placeholder="Event Name" + label="Event Name" + validate={required} + required + ref={this.saveRef} + className={classes.field} + /> + </div> + <div> + <Field + name="start" + component={DateTimePickerRow} + placeholder="Start Date" + value={selectedDate} + onChange={this.onChangeDate} + label="Start Date" + className={classes.field} + /> + </div> + <div> + <Field + name="end" + component={DateTimePickerRow} + placeholder="End Date" + value={selectedDate} + onChange={this.onChangeDate} + label="End Date" + className={classes.field} + /> + </div> + <div className={classes.fieldBasic}> + <FormLabel component="label">Label Color</FormLabel> + <Field name="hexColor" className={classes.inlineWrap} component={renderRadioGroup}> + <FormControlLabel value="F8BBD0" control={<Radio className={classes.redRadio} classes={{ root: classes.redRadio, checked: classes.checked }} />} label="Red" /> + <FormControlLabel value="C8E6C9" control={<Radio className={classes.greenRadio} classes={{ root: classes.greenRadio, checked: classes.checked }} />} label="Green" /> + <FormControlLabel value="B3E5FC" control={<Radio className={classes.blueRadio} classes={{ root: classes.blueRadio, checked: classes.checked }} />} label="Blue" /> + <FormControlLabel value="D1C4E9" control={<Radio className={classes.violetRadio} classes={{ root: classes.violetRadio, checked: classes.checked }} />} label="Violet" /> + <FormControlLabel value="FFECB3" control={<Radio className={classes.orangeRadio} classes={{ root: classes.orangeRadio, checked: classes.checked }} />} label="Orange" /> + </Field> + </div> + </section> + <div className={css.buttonArea}> + <Button variant="contained" color="secondary" type="submit" disabled={submitting}> + Submit + </Button> + <Button + type="button" + disabled={pristine || submitting} + onClick={reset} + > + Reset + </Button> + </div> + </form> + </div> + ); + } +} + +AddEventForm.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, +}; + +const AddEventFormRedux = reduxForm({ + form: 'immutableAddCalendar', + enableReinitialize: true, +})(AddEventForm); + +const reducer = 'calendar'; +const AddEventInit = connect( + state => ({ + force: state, + initialValues: state.getIn([reducer, 'formValues']) + }), +)(AddEventFormRedux); + +export default withStyles(styles)(AddEventInit); diff --git a/front/odiparpack/app/components/Calendar/DetailEvent.js b/front/odiparpack/app/components/Calendar/DetailEvent.js new file mode 100644 index 0000000..b25d8b9 --- /dev/null +++ b/front/odiparpack/app/components/Calendar/DetailEvent.js @@ -0,0 +1,167 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import Today from '@material-ui/icons/Today'; +import { Typography, IconButton, Menu, MenuItem, Divider, Popover } from '@material-ui/core'; +import styles from './calendar-jss'; + + +const ITEM_HEIGHT = 48; + +class DetailEvent extends React.Component { + state = { + anchorElOpt: null, + }; + + handleClickOpt = event => { + this.setState({ anchorElOpt: event.currentTarget }); + }; + + handleCloseOpt = () => { + this.setState({ anchorElOpt: null }); + }; + + handleDeleteEvent = (event) => { + this.setState({ anchorElOpt: null }); + this.props.remove(event); + this.props.close(); + }; + + render() { + const getDate = date => { + if (date._isAMomentObject) { + return date.format('MMMM Do YYYY'); + } + let dd = date.getDate(); + const monthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + const mm = monthNames[date.getMonth()]; // January is 0! + const yyyy = date.getFullYear(); + + if (dd < 10) { + dd = '0' + dd; + } + + const convertedDate = mm + ', ' + dd + ' ' + yyyy; + + return convertedDate; + }; + + const getTime = time => { + if (time._isAMomentObject) { + return time.format('LT'); + } + let h = time.getHours(); + let m = time.getMinutes(); + + if (h < 10) { + h = '0' + h; + } + + if (m < 10) { + m = '0' + m; + } + + const convertedTime = h + ':' + m; + return convertedTime; + }; + + const { + classes, + anchorEl, + event, + close, + anchorPos + } = this.props; + const { anchorElOpt } = this.state; + return ( + <Popover + open={anchorEl} + anchorReference="anchorPosition" + anchorPosition={anchorPos} + className={classes.eventDetail} + onClose={close} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + > + <IconButton + aria-label="More" + aria-owns={anchorElOpt ? 'long-menu' : null} + aria-haspopup="true" + className={classes.moreOpt} + onClick={this.handleClickOpt} + > + <MoreVertIcon /> + </IconButton> + {event !== null + && ( + <Fragment> + <Menu + id="long-menu" + anchorEl={anchorElOpt} + open={Boolean(anchorElOpt)} + onClose={this.handleCloseOpt} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: 200, + }, + }} + > + <MenuItem onClick={() => this.handleDeleteEvent(event)}> + Delete Event + </MenuItem> + </Menu> + <Typography variant="h5" style={{ background: `#${event.hexColor}` }} className={classes.eventName}> + <Today /> + {' '} + {event.title} + </Typography> + <div className={classes.time}> + <Typography> +Start: + {getDate(event.start)} + {' '} +- + {getTime(event.start)} + </Typography> + <Divider className={classes.divider} /> + <Typography> +End: + {getDate(event.end)} + {' '} +- + {getTime(event.end)} + </Typography> + </div> + </Fragment> + ) + } + </Popover> + ); + } +} + +DetailEvent.propTypes = { + classes: PropTypes.object.isRequired, + anchorEl: PropTypes.bool.isRequired, + anchorPos: PropTypes.object.isRequired, + event: PropTypes.object, + close: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, +}; + +DetailEvent.defaultProps = { + event: null, +}; + +export default withStyles(styles)(DetailEvent); diff --git a/front/odiparpack/app/components/Calendar/EventCalendar.js b/front/odiparpack/app/components/Calendar/EventCalendar.js new file mode 100644 index 0000000..fe7b76e --- /dev/null +++ b/front/odiparpack/app/components/Calendar/EventCalendar.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import BigCalendar from 'react-big-calendar'; +import moment from 'moment'; +import { Paper } from '@material-ui/core'; +import styles from './calendar-jss'; + +BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment)); + +function Event(event) { + return ( + <span className="eventBlock">{event.title}</span> + ); +} + +class EventCalendar extends React.Component { + eventStyleGetter = event => { + const backgroundColor = '#' + event.hexColor; + const style = { + backgroundColor, + }; + return { + style + }; + } + + render() { + const allViews = Object.keys(BigCalendar.Views).map(k => BigCalendar.Views[k]); + const { + classes, + events, + handleEventClick + } = this.props; + return ( + <Paper className={classes.root}> + <BigCalendar + className={classes.calendarWrap} + selectable + events={events} + defaultView="month" + views={allViews} + step={60} + showMultiDayTimes + scrollToTime={new Date(1970, 1, 1, 6)} + defaultDate={new Date(2015, 3, 12)} + onSelectEvent={(selectedEvent) => handleEventClick(selectedEvent)} + eventPropGetter={(this.eventStyleGetter)} + onSelectSlot={slotInfo => console.log( + `selected slot: \n\nstart ${slotInfo.start.toLocaleString()} ` + + `\nend: ${slotInfo.end.toLocaleString()}` + + `\naction: ${slotInfo.action}` + ) + } + components={{ + event: Event + }} + /> + </Paper> + ); + } +} + +EventCalendar.propTypes = { + classes: PropTypes.object.isRequired, + events: PropTypes.array.isRequired, + handleEventClick: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(EventCalendar); diff --git a/front/odiparpack/app/components/Calendar/calendar-jss.js b/front/odiparpack/app/components/Calendar/calendar-jss.js new file mode 100644 index 0000000..36ed0a4 --- /dev/null +++ b/front/odiparpack/app/components/Calendar/calendar-jss.js @@ -0,0 +1,118 @@ +import { + pink as red, + lightGreen as green, + lightBlue as blue, + deepPurple as violet, + orange, +} from '@material-ui/core/colors'; + +const styles = theme => ({ + root: { + padding: 20, + [theme.breakpoints.down('sm')]: { + padding: '20px 8px' + }, + }, + calendarWrap: { + minHeight: 600 + }, + addBtn: { + position: 'fixed', + bottom: 30, + right: 30, + zIndex: 100 + }, + typography: { + margin: theme.spacing(2), + }, + divider: { + margin: '5px 0', + textAlign: 'center' + }, + button: { + margin: theme.spacing(1), + }, + eventName: { + padding: '50px 20px 10px 30px', + minWidth: 400, + color: 'rgba(0, 0, 0, 0.7)', + '& svg': { + top: -2, + position: 'relative' + } + }, + time: { + padding: 20 + }, + moreOpt: { + position: 'absolute', + top: 10, + right: 10 + }, + field: { + width: '100%', + marginBottom: 20 + }, + fieldBasic: { + width: '100%', + marginBottom: 20, + marginTop: 10 + }, + inlineWrap: { + display: 'flex', + flexDirection: 'row' + }, + redRadio: { + color: red[600], + '& svg': { + borderRadius: '50%', + background: red[100], + }, + '&$checked': { + color: red[500], + }, + }, + greenRadio: { + color: green[600], + '& svg': { + borderRadius: '50%', + background: green[100], + }, + '&$checked': { + color: green[500], + }, + }, + blueRadio: { + color: blue[600], + '& svg': { + borderRadius: '50%', + background: blue[100], + }, + '&$checked': { + color: blue[500], + }, + }, + violetRadio: { + color: violet[600], + '& svg': { + borderRadius: '50%', + background: violet[100], + }, + '&$checked': { + color: violet[500], + }, + }, + orangeRadio: { + color: orange[600], + '& svg': { + borderRadius: '50%', + background: orange[100], + }, + '&$checked': { + color: orange[500], + }, + }, + checked: {}, +}); + +export default styles; diff --git a/front/odiparpack/app/components/CardPaper/GeneralCard.js b/front/odiparpack/app/components/CardPaper/GeneralCard.js new file mode 100644 index 0000000..85f7787 --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/GeneralCard.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import FavoriteIcon from '@material-ui/icons/Favorite'; +import ShareIcon from '@material-ui/icons/Share'; +import Comment from '@material-ui/icons/Comment'; +import { Card, CardActions, CardContent, IconButton } from '@material-ui/core'; +import styles from './cardStyle-jss'; + + +class GeneralCard extends React.Component { + render() { + const { + classes, + children, + liked, + shared, + commented + } = this.props; + return ( + <Card className={classes.card}> + <CardContent> + {children} + </CardContent> + <CardActions className={classes.actions}> + <IconButton aria-label="Add to favorites" className={classes.button}> + <FavoriteIcon className={liked > 0 && classes.liked} /> + <span className={classes.num}>{liked}</span> + </IconButton> + <IconButton aria-label="Share" className={classes.button}> + <ShareIcon className={shared > 0 && classes.shared} /> + <span className={classes.num}>{shared}</span> + </IconButton> + <IconButton aria-label="Comment" className={classes.rightIcon}> + <Comment /> + <span className={classes.num}>{commented}</span> + </IconButton> + </CardActions> + </Card> + ); + } +} + +GeneralCard.propTypes = { + classes: PropTypes.object.isRequired, + children: PropTypes.node.isRequired, + liked: PropTypes.number.isRequired, + shared: PropTypes.number.isRequired, + commented: PropTypes.number.isRequired, +}; + +export default withStyles(styles)(GeneralCard); diff --git a/front/odiparpack/app/components/CardPaper/IdentityCard.js b/front/odiparpack/app/components/CardPaper/IdentityCard.js new file mode 100644 index 0000000..4f4bd6c --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/IdentityCard.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import LocalPhone from '@material-ui/icons/LocalPhone'; +import LocationOn from '@material-ui/icons/LocationOn'; +import { + Card, Typography, CardContent, + ListItem, ListItemText, ListItemAvatar, + Avatar, Divider +} from '@material-ui/core'; +import styles from './cardStyle-jss'; + + +class IdentityCard extends React.Component { + render() { + const { + classes, + title, + name, + avatar, + phone, + address, + } = this.props; + return ( + <Card className={classes.card}> + <CardContent> + <Typography variant="subtitle1">{title}</Typography> + <Divider className={classes.divider} /> + <ListItem> + <ListItemAvatar> + <Avatar + alt={name} + src={avatar} + className={classes.avatar} + /> + </ListItemAvatar> + <ListItemText primary="Name" secondary={name} /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <LocalPhone /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Phone" secondary={phone} /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <LocationOn /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Address" secondary={address} /> + </ListItem> + </CardContent> + </Card> + ); + } +} + +IdentityCard.propTypes = { + classes: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + avatar: PropTypes.string.isRequired, + phone: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(IdentityCard); diff --git a/front/odiparpack/app/components/CardPaper/NewsCard.js b/front/odiparpack/app/components/CardPaper/NewsCard.js new file mode 100644 index 0000000..f9994d8 --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/NewsCard.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { Card, CardMedia, CardActions, CardContent, Button } from '@material-ui/core'; +import styles from './cardStyle-jss'; + + +class NewsCard extends React.Component { + render() { + const { + classes, + children, + title, + image, + } = this.props; + return ( + <Card className={classes.cardMedia}> + <CardMedia + className={classes.media} + image={image} + title={title} + /> + <CardContent> + {children} + </CardContent> + <CardActions> + <Button size="small" color="primary"> + Share + </Button> + <Button size="small" color="primary"> + Learn More + </Button> + </CardActions> + </Card> + ); + } +} + +NewsCard.propTypes = { + classes: PropTypes.object.isRequired, + children: PropTypes.node.isRequired, + title: PropTypes.string.isRequired, + image: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(NewsCard); diff --git a/front/odiparpack/app/components/CardPaper/PlayerCard.js b/front/odiparpack/app/components/CardPaper/PlayerCard.js new file mode 100644 index 0000000..eabbb3a --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/PlayerCard.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import SkipPreviousIcon from '@material-ui/icons/SkipPrevious'; +import PlayArrowIcon from '@material-ui/icons/PlayArrow'; +import SkipNextIcon from '@material-ui/icons/SkipNext'; +import { Typography, Card, CardMedia, CardContent, IconButton } from '@material-ui/core'; +import styles from './cardStyle-jss'; + + +class PlayerCard extends React.Component { + state = { expanded: false }; + + handleExpandClick = () => { + this.setState({ expanded: !this.state.expanded }); + }; + + render() { + const { + classes, + theme, + title, + artist, + cover, + } = this.props; + + return ( + <Card className={classes.cardPlayer}> + <div className={classes.details}> + <CardContent className={classes.content}> + <Typography variant="h5">{title}</Typography> + <Typography variant="subtitle1" color="textSecondary"> + {artist} + </Typography> + </CardContent> + <div className={classes.controls}> + <IconButton aria-label="Previous"> + {theme.direction === 'rtl' ? <SkipNextIcon /> : <SkipPreviousIcon />} + </IconButton> + <IconButton aria-label="Play/pause"> + <PlayArrowIcon className={classes.playIcon} /> + </IconButton> + <IconButton aria-label="Next"> + {theme.direction === 'rtl' ? <SkipPreviousIcon /> : <SkipNextIcon />} + </IconButton> + </div> + </div> + <CardMedia + className={classes.cover} + image={cover} + title={title} + /> + </Card> + ); + } +} + +PlayerCard.propTypes = { + classes: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + artist: PropTypes.string.isRequired, + cover: PropTypes.string.isRequired, +}; + +export default withStyles(styles, { withTheme: true })(PlayerCard); diff --git a/front/odiparpack/app/components/CardPaper/PostCard.js b/front/odiparpack/app/components/CardPaper/PostCard.js new file mode 100644 index 0000000..9c8f05b --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/PostCard.js @@ -0,0 +1,142 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import FavoriteIcon from '@material-ui/icons/Favorite'; +import ShareIcon from '@material-ui/icons/Share'; +import Comment from '@material-ui/icons/Comment'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { + Typography, + Card, + Menu, + MenuItem, + CardHeader, + CardMedia, + CardContent, + CardActions, + IconButton, + Avatar, +} from '@material-ui/core'; +import styles from './cardStyle-jss'; + + +const optionsOpt = [ + 'Report this post', + 'Hide this post', + 'Copy link', +]; + +const ITEM_HEIGHT = 48; + +class PostCard extends React.Component { + state = { anchorElOpt: null }; + + handleClickOpt = event => { + this.setState({ anchorElOpt: event.currentTarget }); + }; + + handleCloseOpt = () => { + this.setState({ anchorElOpt: null }); + }; + + render() { + const { + classes, + avatar, + name, + date, + image, + content, + liked, + shared, + commented + } = this.props; + const { anchorElOpt } = this.state; + return ( + <Card className={classes.cardSocmed}> + <CardHeader + avatar={ + <Avatar alt="avatar" src={avatar} className={classes.avatar} /> + } + action={( + <IconButton + aria-label="More" + aria-owns={anchorElOpt ? 'long-menu' : null} + aria-haspopup="true" + className={classes.button} + onClick={this.handleClickOpt} + > + <MoreVertIcon /> + </IconButton> + )} + title={name} + subheader={date} + /> + <Menu + id="long-menu" + anchorEl={anchorElOpt} + open={Boolean(anchorElOpt)} + onClose={this.handleCloseOpt} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: 200, + }, + }} + > + {optionsOpt.map(option => ( + <MenuItem key={option} selected={option === 'Edit Profile'} onClick={this.handleCloseOpt}> + {option} + </MenuItem> + ))} + </Menu> + { image !== '' + && ( + <CardMedia + className={classes.media} + image={image} + title="Contemplative Reptile" + /> + ) + } + <CardContent> + <Typography component="p"> + {content} + </Typography> + </CardContent> + <CardActions className={classes.actions}> + <IconButton aria-label="Add to favorites" className={classes.button}> + <FavoriteIcon className={liked > 0 && classes.liked} /> + <span className={classes.num}>{liked}</span> + </IconButton> + <IconButton aria-label="Share" className={classes.button}> + <ShareIcon className={shared > 0 && classes.shared} /> + <span className={classes.num}>{shared}</span> + </IconButton> + <IconButton aria-label="Comment" className={classes.rightIcon}> + <Comment /> + <span className={classes.num}>{commented}</span> + </IconButton> + </CardActions> + </Card> + ); + } +} + +PostCard.propTypes = { + classes: PropTypes.object.isRequired, + avatar: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, + image: PropTypes.string, + content: PropTypes.string.isRequired, + liked: PropTypes.number.isRequired, + shared: PropTypes.number.isRequired, + commented: PropTypes.number.isRequired, +}; + +PostCard.defaultProps = { + image: '' +}; + +export default withStyles(styles)(PostCard); diff --git a/front/odiparpack/app/components/CardPaper/ProductCard.js b/front/odiparpack/app/components/CardPaper/ProductCard.js new file mode 100644 index 0000000..967e2fd --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/ProductCard.js @@ -0,0 +1,134 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { isWidthUp } from '@material-ui/core/withWidth'; +import classNames from 'classnames'; +import AddShoppingCart from '@material-ui/icons/AddShoppingCart'; +import Type from 'ba-styles/Typography.scss'; +import { + Typography, + withWidth, + Card, + IconButton, + Tooltip, + CardMedia, + CardActions, + CardContent, + Chip, + Fab, + Button, +} from '@material-ui/core'; +import Rating from '../Rating/Rating'; +import styles from './cardStyle-jss'; + + +class ProductCard extends React.Component { + render() { + const { + classes, + discount, + soldout, + thumbnail, + name, + desc, + ratting, + price, + prevPrice, + list, + detailOpen, + addToCart, + width, + } = this.props; + return ( + <Card className={classNames(classes.cardProduct, isWidthUp('sm', width) && list ? classes.cardList : '')}> + <div className={classes.status}> + {discount !== '' && ( + <Chip label={'Discount ' + discount} className={classes.chipDiscount} /> + )} + {soldout && ( + <Chip label="Sold Out" className={classes.chipSold} /> + )} + </div> + <CardMedia + className={classes.mediaProduct} + image={thumbnail} + title={name} + /> + <CardContent className={classes.floatingButtonWrap}> + {!soldout && ( + <Tooltip title="Add to cart" placement="top"> + <Fab onClick={addToCart} size="small" color="secondary" aria-label="add" className={classes.buttonAdd}> + <AddShoppingCart /> + </Fab> + </Tooltip> + )} + <Typography noWrap gutterBottom variant="h5" className={classes.title} component="h2"> + {name} + </Typography> + <Typography component="p" className={classes.desc}> + {desc} + </Typography> + <div className={classes.ratting}> + <Rating value={ratting} max={5} readOnly /> + </div> + </CardContent> + <CardActions className={classes.price}> + <Typography variant="h5"> + <span> +$ + {price} + </span> + </Typography> + {prevPrice > 0 && ( + <Typography variant="caption" component="h5"> + <span className={Type.lineThrought}> +$ + {prevPrice} + </span> + </Typography> + )} + <div className={classes.rightAction}> + <Button size="small" variant="outlined" color="secondary" onClick={detailOpen}> + See Detail + </Button> + {!soldout && ( + <Tooltip title="Add to cart" placement="top"> + <IconButton color="secondary" onClick={addToCart} className={classes.buttonAddList}> + <AddShoppingCart /> + </IconButton> + </Tooltip> + )} + </div> + </CardActions> + </Card> + ); + } +} + +ProductCard.propTypes = { + classes: PropTypes.object.isRequired, + discount: PropTypes.string, + width: PropTypes.string.isRequired, + soldout: PropTypes.bool, + thumbnail: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + desc: PropTypes.string.isRequired, + ratting: PropTypes.number.isRequired, + price: PropTypes.number.isRequired, + prevPrice: PropTypes.number, + list: PropTypes.bool, + detailOpen: PropTypes.func, + addToCart: PropTypes.func, +}; + +ProductCard.defaultProps = { + discount: '', + soldout: false, + prevPrice: 0, + list: false, + detailOpen: () => (false), + addToCart: () => (false), +}; + +const ProductCardResponsive = withWidth()(ProductCard); +export default withStyles(styles)(ProductCardResponsive); diff --git a/front/odiparpack/app/components/CardPaper/ProfileCard.js b/front/odiparpack/app/components/CardPaper/ProfileCard.js new file mode 100644 index 0000000..7827941 --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/ProfileCard.js @@ -0,0 +1,99 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Type from 'ba-styles/Typography.scss'; +import VerifiedUser from '@material-ui/icons/VerifiedUser'; +import SupervisorAccount from '@material-ui/icons/SupervisorAccount'; +import Favorite from '@material-ui/icons/Favorite'; +import PhotoLibrary from '@material-ui/icons/PhotoLibrary'; +import { + Typography, + Card, + CardMedia, + CardContent, + CardActions, + Button, + Avatar, + BottomNavigation, + BottomNavigationAction, + Divider, +} from '@material-ui/core'; +import styles from './cardStyle-jss'; + + +class ProfileCard extends React.Component { + state = { expanded: false }; + + handleExpandClick = () => { + this.setState({ expanded: !this.state.expanded }); + }; + + render() { + const { + classes, + cover, + avatar, + name, + title, + connection, + isVerified, + btnText + } = this.props; + + return ( + <Card className={classes.cardSocmed}> + <CardMedia + className={classes.mediaProfile} + image={cover} + title="cover" + /> + <CardContent className={classes.contentProfile}> + <Avatar alt="avatar" src={avatar} className={classes.avatarBig} /> + <Typography variant="h6" className={classes.name} gutterBottom> + {name} + {isVerified && <VerifiedUser className={classes.verified} />} + </Typography> + <Typography className={classes.subheading} gutterBottom> + <span className={Type.regular}>{title}</span> + </Typography> + <Typography variant="caption" component="p"> + {connection} + {' '} +connection + </Typography> + <Button className={classes.buttonProfile} size="large" variant="outlined" color="secondary"> + {btnText} + </Button> + </CardContent> + <Divider /> + <CardActions> + <BottomNavigation + showLabels + className={classes.bottomLink} + > + <BottomNavigationAction label="20 Connection" icon={<SupervisorAccount />} /> + <BottomNavigationAction label="10 Favorites" icon={<Favorite />} /> + <BottomNavigationAction label="5 Albums" icon={<PhotoLibrary />} /> + </BottomNavigation> + </CardActions> + </Card> + ); + } +} + +ProfileCard.propTypes = { + classes: PropTypes.object.isRequired, + cover: PropTypes.string.isRequired, + avatar: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + connection: PropTypes.number.isRequired, + btnText: PropTypes.string.isRequired, + isVerified: PropTypes.bool +}; + +ProfileCard.defaultProps = { + isVerified: false +}; + +export default withStyles(styles)(ProfileCard); diff --git a/front/odiparpack/app/components/CardPaper/VideoCard.js b/front/odiparpack/app/components/CardPaper/VideoCard.js new file mode 100644 index 0000000..69aa60c --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/VideoCard.js @@ -0,0 +1,90 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import PlayArrowIcon from '@material-ui/icons/PlayArrow'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { red } from '@material-ui/core/colors'; + +import { Card, CardHeader, CardMedia, IconButton, Avatar } from '@material-ui/core'; + +const styles = theme => ({ + playIcon: { + height: 38, + width: 38, + }, + cardSocmed: { + maxWidth: 400, + }, + media: { + height: 0, + paddingTop: '56.25%', // 16:9 + position: 'relative', + }, + avatar: { + backgroundColor: red[500], + }, + playBtn: { + position: 'absolute', + top: '50%', + left: '50%', + width: 64, + height: 64, + transform: 'translate(-50%, -50%)', + '& svg': { + color: theme.palette.common.white, + fontSize: 64 + } + } +}); + +class VideoCard extends React.Component { + state = { expanded: false }; + + handleExpandClick = () => { + this.setState({ expanded: !this.state.expanded }); + }; + + render() { + const { + classes, + title, + cover, + date + } = this.props; + + return ( + <Card className={classes.cardSocmed}> + <CardMedia + className={classes.media} + image={cover} + title={title} + > + <IconButton className={classes.playBtn}><PlayArrowIcon /></IconButton> + </CardMedia> + <CardHeader + avatar={( + <Avatar aria-label="Recipe" className={classes.avatar}> + R + </Avatar> + )} + action={( + <IconButton> + <MoreVertIcon /> + </IconButton> + )} + title={title} + subheader={date} + /> + </Card> + ); + } +} + +VideoCard.propTypes = { + classes: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + cover: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(VideoCard); diff --git a/front/odiparpack/app/components/CardPaper/cardStyle-jss.js b/front/odiparpack/app/components/CardPaper/cardStyle-jss.js new file mode 100644 index 0000000..df367a0 --- /dev/null +++ b/front/odiparpack/app/components/CardPaper/cardStyle-jss.js @@ -0,0 +1,188 @@ +import { pink, lightGreen, blueGrey as dark } from '@material-ui/core/colors'; + +const styles = theme => ({ + divider: { + margin: `${theme.spacing(3)}px 0` + }, + card: { + minWidth: 275, + }, + liked: { + color: pink[500] + }, + shared: { + color: lightGreen[500] + }, + num: { + fontSize: 14, + marginLeft: 5 + }, + rightIcon: { + marginLeft: 'auto', + display: 'flex', + alignItems: 'center' + }, + button: { + marginRight: theme.spacing(1) + }, + media: { + height: 0, + paddingTop: '56.25%', // 16:9 + }, + cardPlayer: { + display: 'flex', + justifyContent: 'space-between' + }, + details: { + display: 'flex', + flexDirection: 'column', + }, + content: { + flex: '1 0 auto', + }, + cover: { + width: 150, + height: 150, + }, + controls: { + display: 'flex', + alignItems: 'center', + paddingLeft: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, + playIcon: { + height: 38, + width: 38, + }, + cardSocmed: { + minWidth: 275, + }, + cardProduct: { + position: 'relative' + }, + mediaProduct: { + height: 0, + paddingTop: '60.25%', // 16:9 + }, + rightAction: { + '&:not(:first-child)': { + marginLeft: 'auto', + display: 'flex', + alignItems: 'center' + } + }, + floatingButtonWrap: { + position: 'relative', + paddingTop: 50 + }, + buttonAdd: { + position: 'absolute', + right: 20, + top: -20, + }, + buttonAddList: { + display: 'none', + marginLeft: 10 + }, + title: { + fontSize: 20, + height: 30, + }, + ratting: { + margin: '10px 0', + '& button': { + width: 24, + height: 24 + } + }, + status: { + position: 'absolute', + right: 0, + top: 0, + padding: 10, + '& > *': { + margin: 5 + } + }, + desc: { + height: 45, + overflow: 'hidden' + }, + chipDiscount: { + background: theme.palette.primary.light, + color: theme.palette.primary.dark, + }, + chipSold: { + background: dark[500], + color: theme.palette.getContrastText(dark[500]), + }, + contentProfle: { + flex: '1 0 auto', + textAlign: 'center' + }, + mediaProfile: { + height: 0, + paddingTop: '36.25%', // 16:9 + }, + actions: { + display: 'flex', + }, + avatarBig: { + width: 80, + height: 80, + margin: '-56px auto 10px', + background: theme.palette.secondary.dark + }, + name: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }, + buttonProfile: { + margin: 20 + }, + bottomLink: { + width: '100%', + }, + price: { + padding: theme.spacing(2), + paddingBottom: 20 + }, + contentProfile: { + textAlign: 'center' + }, + verified: { + fontSize: 16, + color: theme.palette.primary.main + }, + cardList: { + display: 'flex', + justifyContent: 'space-between', + '& $buttonAddList': { + display: 'inline-block' + }, + '& $floatingButtonWrap': { + flex: 1, + }, + '& $buttonAdd': { + display: 'none' + }, + '& $status': { + right: 'auto', + left: 0, + }, + '& $mediaProduct': { + width: 300, + paddingTop: '21.25%' + }, + '& $price': { + flexDirection: 'column', + justifyContent: 'center', + '& button': { + marginTop: 20 + } + } + }, +}); + +export default styles; diff --git a/front/odiparpack/app/components/Cart/Cart.js b/front/odiparpack/app/components/Cart/Cart.js new file mode 100644 index 0000000..d477a51 --- /dev/null +++ b/front/odiparpack/app/components/Cart/Cart.js @@ -0,0 +1,137 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import DeleteIcon from '@material-ui/icons/Delete'; +import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; +import Type from 'ba-styles/Typography.scss'; +import { + Menu, + Typography, + Button, + ListSubheader, + List, + ListItem, + ListItemText, + ListItemSecondaryAction, + IconButton, + Divider, +} from '@material-ui/core'; +import styles from './cart-jss'; + + +class Cart extends React.Component { + render() { + const { + classes, + anchorEl, + close, + dataCart, + removeItem, + totalPrice, + checkout + } = this.props; + + const getCartItem = dataArray => dataArray.map((item, index) => ( + <Fragment key={index.toString()}> + <ListItem> + <figure> + <img src={item.get('thumbnail')} alt="thumb" /> + </figure> + <ListItemText + primary={item.get('name')} + secondary={`Quantity: ${item.get('quantity')} Item - USD ${item.get('price') * item.get('quantity')}`} + className={classes.itemText} + /> + <ListItemSecondaryAction> + <IconButton aria-label="Comments" onClick={() => removeItem(item)}> + <DeleteIcon /> + </IconButton> + </ListItemSecondaryAction> + </ListItem> + <li> + <Divider /> + </li> + </Fragment> + )); + return ( + <Menu + id="cart-menu" + anchorEl={anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + open={Boolean(anchorEl)} + onClose={close} + className={classes.cartPanel} + > + <List + component="ul" + subheader={( + <ListSubheader component="div"> + <ShoppingCartIcon /> + {' '} +Total: + {' '} + {dataCart.size} + {' '} +Unique items in Cart + </ListSubheader> + )} + className={classes.cartWrap} + > + { + dataCart.size < 1 ? ( + <div className={classes.empty}> + <Typography variant="subtitle1">Empty Cart</Typography> + <Typography variant="caption">Your shopping items will be listed here</Typography> + </div> + ) : ( + <Fragment> + {getCartItem(dataCart)} + <ListItem className={classes.totalPrice}> + <Typography variant="subtitle1"> + Total : + {' '} + <span className={Type.bold}> +$ + {totalPrice} + </span> + </Typography> + </ListItem> + <li> + <Divider /> + </li> + <ListItem> + <Button fullWidth className={classes.button} variant="contained" onClick={() => checkout()} color="secondary"> + Checkout + </Button> + </ListItem> + </Fragment> + ) + } + </List> + </Menu> + ); + } +} + +Cart.propTypes = { + classes: PropTypes.object.isRequired, + dataCart: PropTypes.object.isRequired, + anchorEl: PropTypes.object, + close: PropTypes.func.isRequired, + removeItem: PropTypes.func.isRequired, + checkout: PropTypes.func.isRequired, + totalPrice: PropTypes.number.isRequired, +}; + +Cart.defaultProps = { + anchorEl: null, +}; + +export default withStyles(styles)(Cart); diff --git a/front/odiparpack/app/components/Cart/cart-jss.js b/front/odiparpack/app/components/Cart/cart-jss.js new file mode 100644 index 0000000..aef71cd --- /dev/null +++ b/front/odiparpack/app/components/Cart/cart-jss.js @@ -0,0 +1,38 @@ +const styles = theme => ({ + totalPrice: { + background: theme.palette.grey[200], + textAlign: 'right', + display: 'block' + }, + cartWrap: { + [theme.breakpoints.up('sm')]: { + width: 400, + }, + '&:focus': { + outline: 'none' + } + }, + itemText: { + marginRight: 30, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + width: 220 + }, + cartPanel: { + '& figure': { + width: 120, + height: 70, + overflow: 'hidden', + '& img': { + maxWidth: '100%' + } + } + }, + empty: { + textAlign: 'center', + padding: 20 + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Chat/ChatHeader.js b/front/odiparpack/app/components/Chat/ChatHeader.js new file mode 100644 index 0000000..b3456e9 --- /dev/null +++ b/front/odiparpack/app/components/Chat/ChatHeader.js @@ -0,0 +1,122 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import ArrowBack from '@material-ui/icons/ArrowBack'; +import { Typography, AppBar, Menu, MenuItem, Avatar, IconButton, Toolbar } from '@material-ui/core'; +import styles from '../Contact/contact-jss'; + + +const optionsOpt = [ + 'Delete Conversation', + 'Option 1', + 'Option 2', + 'Option 3', +]; + +const ITEM_HEIGHT = 48; + +class ChatHeader extends React.Component { + state = { + anchorElOpt: null, + }; + + handleClickOpt = event => { + this.setState({ anchorElOpt: event.currentTarget }); + }; + + handleCloseOpt = () => { + this.setState({ anchorElOpt: null }); + }; + + handleRemove = (person) => { + this.props.remove(person); + } + + render() { + const { + classes, + chatSelected, + dataContact, + showMobileDetail, + hideDetail, + } = this.props; + const { anchorElOpt } = this.state; + return ( + <AppBar + position="absolute" + className={classNames(classes.appBar, classes.appBarShift)} + > + <Toolbar> + {showMobileDetail && ( + <IconButton + color="inherit" + aria-label="open drawer" + onClick={() => hideDetail()} + className={classes.navIconHide} + > + <ArrowBack /> + </IconButton> + )} + <Avatar alt="avatar" src={dataContact.getIn([chatSelected, 'avatar'])} className={classes.avatar} /> + <Typography variant="h6" className={classes.flex} color="inherit" noWrap> + {dataContact.getIn([chatSelected, 'name'])} + <Typography variant="caption" component="p" className={classes.status} color="inherit" noWrap> + <span className={classes.online} /> + {' '} +Online + </Typography> + </Typography> + <IconButton + aria-label="More" + aria-owns={anchorElOpt ? 'long-menu' : null} + aria-haspopup="true" + className={classes.button} + onClick={this.handleClickOpt} + > + <MoreVertIcon color="inherit" /> + </IconButton> + </Toolbar> + <Menu + id="long-menu" + anchorEl={anchorElOpt} + open={Boolean(anchorElOpt)} + onClose={this.handleCloseOpt} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: 200, + }, + }} + > + {optionsOpt.map(option => { + if (option === 'Delete Conversation') { + return ( + <MenuItem key={option} onClick={this.handleRemove}> + {option} + </MenuItem> + ); + } + return ( + <MenuItem key={option} selected={option === 'Edit Profile'} onClick={this.handleCloseOpt}> + {option} + </MenuItem> + ); + })} + </Menu> + </AppBar> + ); + } +} + +ChatHeader.propTypes = { + classes: PropTypes.object.isRequired, + dataContact: PropTypes.object.isRequired, + showMobileDetail: PropTypes.bool.isRequired, + hideDetail: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + chatSelected: PropTypes.number.isRequired, +}; + +export default withStyles(styles)(ChatHeader); diff --git a/front/odiparpack/app/components/Chat/ChatRoom.js b/front/odiparpack/app/components/Chat/ChatRoom.js new file mode 100644 index 0000000..9abef89 --- /dev/null +++ b/front/odiparpack/app/components/Chat/ChatRoom.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import Send from '@material-ui/icons/Send'; +import dummyContents from 'ba-api/dummyContents'; +import Type from 'ba-styles/Typography.scss'; +import { Avatar, Typography, Paper, Tooltip, IconButton } from '@material-ui/core'; +import MessageField from './MessageField'; +import styles from './chatStyle-jss'; + + +class ChatRoom extends React.Component { + constructor() { + super(); + this.state = { message: '' }; + this.handleWrite = this.handleWrite.bind(this); + } + + handleWrite = (e, value) => { + this.setState({ message: value }); + }; + + resetInput = () => { + const ctn = document.getElementById('roomContainer'); + this.setState({ message: '' }); + this._field.setState({ value: '' }); + setTimeout(() => { + ctn.scrollTo(0, ctn.scrollHeight); + }, 300); + } + + sendMessageByEnter = (event, message) => { + if (event.key === 'Enter' && event.target.value !== '') { + this.props.sendMessage(message.__html); + this.resetInput(); + } + } + + sendMessage = message => { + this.props.sendMessage(message.__html); + this.resetInput(); + } + + render() { + const html = { __html: this.state.message }; + const { + classes, + dataChat, + chatSelected, + dataContact, + showMobileDetail, + } = this.props; + const { message } = this.state; + const getChat = dataArray => dataArray.map(data => { + const renderHTML = { __html: data.get('message') }; + return ( + <li className={data.get('from') === 'contact' ? classes.from : classes.to} key={data.get('id')}> + <time dateTime={data.get('date') + ' ' + data.get('time')}>{data.get('date') + ' ' + data.get('time')}</time> + {data.get('from') === 'contact' + ? <Avatar alt="avatar" src={dataContact.getIn([chatSelected, 'avatar'])} className={classes.avatar} /> + : <Avatar alt="avatar" src={dummyContents.user.avatar} className={classes.avatar} /> + } + <div className={classes.talk}> + <p><span dangerouslySetInnerHTML={renderHTML} /></p> + </div> + </li> + ); + }); + return ( + <div className={classNames(classes.root, showMobileDetail ? classes.detailPopup : '')}> + <ul className={classes.chatList} id="roomContainer"> + {dataChat.size > 0 ? getChat(dataChat) : (<Typography variant="caption" component="p" className={Type.textCenter}>{'You haven\'t made any conversation yet'}</Typography>)} + </ul> + <Paper className={classes.writeMessage}> + <MessageField + onChange={this.handleWrite} + ref={(_field) => { this._field = _field; return this._field; }} + placeholder="Type a message" + fieldType="input" + value={message} + onKeyPress={(event) => this.sendMessageByEnter(event, html)} + /> + <Tooltip id="tooltip-send" title="Send"> + <div> + <IconButton size="small" color="secondary" disabled={message === ''} onClick={() => this.sendMessage(html)} aria-label="send" className={classes.sendBtn}> + <Send /> + </IconButton> + </div> + </Tooltip> + </Paper> + </div> + ); + } +} + +ChatRoom.propTypes = { + classes: PropTypes.object.isRequired, + dataChat: PropTypes.object.isRequired, + showMobileDetail: PropTypes.bool.isRequired, + chatSelected: PropTypes.number.isRequired, + dataContact: PropTypes.object.isRequired, + sendMessage: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(ChatRoom); diff --git a/front/odiparpack/app/components/Chat/MessageField.js b/front/odiparpack/app/components/Chat/MessageField.js new file mode 100644 index 0000000..947bed4 --- /dev/null +++ b/front/odiparpack/app/components/Chat/MessageField.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import 'ba-styles/vendors/emoji-picker-react/emoji-picker-react.css'; + +class MessageField extends Component { + constructor(props) { + super(props); + + this.state = { + value: props.value || '', + }; + + this.onChange = this.onChange.bind(this); + } + + onChange(e) { + const { val } = this.state; + const { onChange } = this.props; + const value = e ? e.target.value : val; + + this.setState({ value }, () => { + if (typeof onChange === 'function') { + onChange(e, value); + } + }); + } + + onPickerkeypress(e) { + if (e.keyCode === 27 || e.which === 27 || e.key === 'Escape' || e.code === 'Escape') { + this.closePicker(); + } + } + + render() { + const { + onChange, + fieldType, + ...rest + } = this.props; + + const className = `emoji-text-field emoji-${fieldType}`; + const { value } = this.state; + const isInput = fieldType === 'input'; + const ref = (_field) => { + this._field = _field; + return this._field; + }; + + return ( + <div className={className}> + { (isInput) && (<input {...rest} onChange={this.onChange} type="text" ref={ref} value={value} />) } + { (!isInput) && (<textarea {...rest} onChange={this.onChange} ref={ref} value={value} />) } + </div> + ); + } +} + +MessageField.propTypes = { + value: PropTypes.string, + onChange: PropTypes.func, + fieldType: PropTypes.string.isRequired +}; + +MessageField.defaultProps = { + value: '', + onChange: () => {}, +}; + +export default MessageField; diff --git a/front/odiparpack/app/components/Chat/chatStyle-jss.js b/front/odiparpack/app/components/Chat/chatStyle-jss.js new file mode 100644 index 0000000..360b170 --- /dev/null +++ b/front/odiparpack/app/components/Chat/chatStyle-jss.js @@ -0,0 +1,148 @@ +import { lighten } from '@material-ui/core/styles/colorManipulator'; +const styles = theme => ({ + root: { + flexGrow: 1, + display: 'flex', + flexDirection: 'column', + position: 'relative', + backgroundColor: lighten(theme.palette.secondary.light, 0.9), + }, + chatList: { + padding: 24, + paddingTop: 110, + overflow: 'auto', + height: 580, + '& li': { + marginBottom: theme.spacing(6), + display: 'flex', + position: 'relative', + '& time': { + position: 'absolute', + top: -20, + color: theme.palette.grey[500], + fontSize: 11 + } + }, + }, + detailPopup: { + [theme.breakpoints.down('xs')]: { + position: 'absolute', + top: 0, + left: 0, + zIndex: 1200, + width: '100%', + overflow: 'auto', + height: 575 + } + }, + talk: { + flex: 1, + '& p': { + marginBottom: 10, + position: 'relative', + '& span': { + padding: 10, + borderRadius: 10, + display: 'inline-block' + } + } + }, + avatar: {}, + from: { + '& time': { + left: 60, + }, + '& $avatar': { + marginRight: 20 + }, + '& $talk': { + '& > p': { + '& span': { + background: theme.palette.secondary.light, + border: `1px solid ${lighten(theme.palette.secondary.main, 0.5)}`, + }, + '&:first-child': { + '& span': { + borderTopLeftRadius: 0, + }, + '&:before': { + content: '""', + borderRight: `11px solid ${lighten(theme.palette.secondary.main, 0.5)}`, + borderBottom: '17px solid transparent', + position: 'absolute', + left: -11, + top: 0 + }, + '&:after': { + content: '""', + borderRight: `10px solid ${theme.palette.secondary.light}`, + borderBottom: '15px solid transparent', + position: 'absolute', + left: -9, + top: 1 + }, + } + } + } + }, + to: { + flexDirection: 'row-reverse', + '& time': { + right: 60, + }, + '& $avatar': { + marginLeft: 20 + }, + '& $talk': { + textAlign: 'right', + '& > p': { + '& span': { + textAlign: 'left', + background: theme.palette.primary.light, + border: `1px solid ${lighten(theme.palette.primary.main, 0.5)}`, + }, + '&:first-child': { + '& span': { + borderTopRightRadius: 0, + }, + '&:before': { + content: '""', + borderLeft: `11px solid ${lighten(theme.palette.primary.main, 0.5)}`, + borderBottom: '17px solid transparent', + position: 'absolute', + right: -11, + top: 0 + }, + '&:after': { + content: '""', + borderLeft: `10px solid ${theme.palette.primary.light}`, + borderBottom: '15px solid transparent', + position: 'absolute', + right: -9, + top: 1 + }, + } + } + } + }, + messageBox: { + border: 'none', + padding: 0, + outline: 'none', + width: '100%', + '&:after, &:before': { + display: 'none' + } + }, + writeMessage: { + position: 'relative', + bottom: 16, + display: 'flex', + minHeight: 55, + margin: '0 16px', + alignItems: 'center', + padding: '0 10px', + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Chat/svg/trigger-opaque.svg b/front/odiparpack/app/components/Chat/svg/trigger-opaque.svg new file mode 100644 index 0000000..1540f5b --- /dev/null +++ b/front/odiparpack/app/components/Chat/svg/trigger-opaque.svg @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> +<circle style="fill:#F7B239;" cx="256.004" cy="256.004" r="246.855"/> +<path style="fill:#E09B2D;" d="M126.308,385.694c-88.802-88.802-95.799-228.426-20.999-325.241 + c-8.286,6.401-16.258,13.399-23.858,20.999c-96.401,96.401-96.401,252.698,0,349.099s252.698,96.401,349.099,0 + c7.599-7.599,14.597-15.573,20.999-23.858C354.734,481.492,215.108,474.495,126.308,385.694z"/> +<path style="fill:#4D4D4D;" d="M256.001,184.864L256.001,184.864c14.551,0,29.02-1.978,43.1-5.65 + c53.478-13.946,112.377-5.29,149.086,3.183c19.574,4.519,30.868,25.04,24.163,43.978l-20.669,58.372 + c-4.75,13.414-17.654,22.198-31.877,21.698l-94.167-3.316c-15.87-0.559-29.283-11.933-32.426-27.5l-4.758-23.558 + c-3.119-15.446-16.695-26.551-32.452-26.548l0,0l0,0c-15.759,0-29.333,11.108-32.453,26.554l-4.757,23.552 + c-3.144,15.565-16.557,26.94-32.426,27.5l-94.167,3.316c-14.222,0.501-27.127-8.282-31.876-21.698l-20.669-58.372 + c-6.706-18.937,4.589-39.459,24.163-43.978c36.709-8.472,95.607-17.129,149.086-3.183 + C226.981,182.885,241.449,184.864,256.001,184.864L256.001,184.864z"/> +<path d="M255.999,512C114.841,512,0,397.16,0,256.001S114.841,0,255.999,0C397.159,0,512,114.841,512,256.001 + S397.159,512,255.999,512z M255.999,18.299c-131.068,0-237.7,106.632-237.7,237.702s106.632,237.702,237.7,237.702 + c131.069,0,237.702-106.632,237.702-237.702S387.068,18.299,255.999,18.299z"/> +<path d="M256.269,391.607c-2.716,0-5.443-0.051-8.186-0.155c-42.952-1.624-80.516-15.925-103.062-39.237 + c-3.513-3.633-3.416-9.425,0.216-12.937c3.633-3.515,9.425-3.416,12.937,0.216c40.228,41.595,138.107,45.631,187.018,7.708 + c2.506-1.942,7.259-6.399,8.879-7.952c3.646-3.496,9.437-3.376,12.936,0.272c3.498,3.648,3.376,9.439-0.272,12.936 + c-0.666,0.638-6.616,6.324-10.33,9.204C331.637,380.865,295.496,391.607,256.269,391.607z"/> +<path d="M420.986,315.612c-0.501,0-1-0.009-1.504-0.026l-94.167-3.316c-20.185-0.711-37.075-15.035-41.073-34.832l-4.758-23.558 + c-2.248-11.132-12.123-19.21-23.478-19.21c-0.001,0-0.002,0-0.004,0c-11.361,0-21.238,8.082-23.486,19.215l-4.757,23.553 + c-3.998,19.798-20.888,34.12-41.073,34.832l-94.167,3.316c-18.28,0.615-34.708-10.522-40.823-27.787l-20.669-58.372 + c-4.061-11.469-3.034-24.162,2.817-34.826c5.859-10.681,16.034-18.378,27.913-21.121c40.032-9.239,99.151-17.283,153.454-3.122 + c13.623,3.552,27.347,5.354,40.791,5.354l0,0c13.444,0,27.168-1.802,40.791-5.354c54.302-14.16,113.421-6.118,153.452,3.121 + c11.88,2.742,22.054,10.44,27.913,21.121c5.851,10.665,6.878,23.357,2.817,34.826l-20.669,58.372 + C454.359,304.588,438.678,315.611,420.986,315.612z M150.311,180.969c-23.43,0-51.525,2.745-84.439,10.341 + c-6.805,1.57-12.631,5.978-15.986,12.092c-3.395,6.188-3.966,13.261-1.61,19.916l20.669,58.372 + c3.434,9.697,12.647,15.962,22.929,15.607l94.167-3.316c11.687-0.411,21.465-8.704,23.78-20.166l4.757-23.553 + c3.966-19.639,21.387-33.892,41.422-33.892c0.002,0,0.005,0,0.007,0c20.03,0,37.447,14.25,41.413,33.886l4.758,23.558 + c2.314,11.463,12.093,19.756,23.78,20.167l94.167,3.316c10.294,0.367,19.496-5.909,22.93-15.607l20.669-58.372 + c2.357-6.656,1.785-13.728-1.61-19.916c-3.355-6.114-9.181-10.522-15.986-12.092c-38.033-8.777-94.012-16.469-144.719-3.245 + c-15.131,3.945-30.408,5.946-45.409,5.946c-15,0-30.279-2.001-45.41-5.946C195.972,184.255,175.932,180.969,150.311,180.969z"/> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +</svg> diff --git a/front/odiparpack/app/components/Chat/svg/trigger-transparent.svg b/front/odiparpack/app/components/Chat/svg/trigger-transparent.svg new file mode 100644 index 0000000..8555ab9 --- /dev/null +++ b/front/odiparpack/app/components/Chat/svg/trigger-transparent.svg @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> +<g> + <g> + <path d="M255.999,0C114.841,0,0,114.841,0,256.001S114.841,512,255.999,512C397.159,512,512,397.16,512,256.001 + S397.159,0,255.999,0z M255.999,493.702c-131.068,0-237.7-106.632-237.7-237.702s106.632-237.702,237.7-237.702 + c131.069,0,237.702,106.632,237.702,237.702S387.068,493.702,255.999,493.702z"/> + </g> +</g> +<g> + <g> + <path d="M367.008,339.521c-3.499-3.648-9.29-3.768-12.936-0.272c-1.62,1.553-6.373,6.009-8.879,7.952 + c-48.911,37.923-146.79,33.888-187.018-7.708c-3.512-3.632-9.304-3.731-12.937-0.216c-3.632,3.512-3.729,9.304-0.216,12.937 + c22.546,23.312,60.11,37.613,103.062,39.237c2.742,0.104,5.47,0.155,8.186,0.155c39.227,0,75.368-10.742,100.136-29.945 + c3.715-2.88,9.664-8.566,10.33-9.204C370.383,348.96,370.505,343.169,367.008,339.521z"/> + </g> +</g> +<g> + <g> + <path d="M478.16,194.601c-5.859-10.681-16.034-18.378-27.913-21.121c-40.031-9.239-99.151-17.28-153.453-3.121 + c-13.623,3.552-27.347,5.354-40.791,5.354s-27.168-1.802-40.791-5.354c-54.303-14.161-113.421-6.117-153.454,3.122 + c-11.88,2.742-22.054,10.44-27.913,21.121c-5.851,10.664-6.878,23.357-2.817,34.826L51.697,287.8 + c6.114,17.265,22.543,28.401,40.823,27.787l94.167-3.316c20.185-0.711,37.075-15.033,41.073-34.832l4.757-23.553 + c2.248-11.133,12.125-19.215,23.486-19.215c0.001,0,0.002,0,0.004,0c11.355,0,21.229,8.078,23.478,19.21l4.758,23.558 + c3.998,19.797,20.888,34.12,41.073,34.832l94.167,3.316c0.504,0.017,1.003,0.026,1.504,0.026 + c17.692-0.001,33.373-11.023,39.321-27.813l20.669-58.372C485.038,217.958,484.011,205.266,478.16,194.601z M463.725,223.318 + l-20.669,58.372c-3.434,9.697-12.636,15.974-22.93,15.607l-94.167-3.316c-11.687-0.411-21.466-8.704-23.78-20.167l-4.758-23.558 + c-3.966-19.636-21.383-33.886-41.413-33.886c-0.002,0-0.005,0-0.007,0c-20.035,0-37.456,14.254-41.422,33.892l-4.757,23.553 + c-2.315,11.461-12.093,19.754-23.78,20.166l-94.167,3.316c-10.282,0.355-19.495-5.909-22.929-15.607l-20.669-58.372 + c-2.356-6.655-1.785-13.728,1.61-19.915c3.355-6.114,9.181-10.522,15.986-12.092c32.914-7.597,61.009-10.341,84.439-10.341 + c25.621,0,45.661,3.285,60.28,7.096c15.131,3.945,30.409,5.946,45.41,5.946c15,0,30.278-2.001,45.409-5.946 + c50.707-13.224,106.686-5.532,144.719,3.245c6.805,1.57,12.631,5.978,15.986,12.092 + C465.509,209.59,466.081,216.662,463.725,223.318z"/> + </g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +</svg> diff --git a/front/odiparpack/app/components/Contact/AddContact.js b/front/odiparpack/app/components/Contact/AddContact.js new file mode 100644 index 0000000..2690f1d --- /dev/null +++ b/front/odiparpack/app/components/Contact/AddContact.js @@ -0,0 +1,82 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Add from '@material-ui/icons/Add'; +import { Tooltip, Fab } from '@material-ui/core'; +import AddContactForm from './AddContactForm'; +import FloatingPanel from '../Panel/FloatingPanel'; +import styles from './contact-jss'; + + +class AddContact extends React.Component { + constructor(props) { + super(props); + this.state = { + img: '', + files: [] + }; + this.onDrop = this.onDrop.bind(this); + } + + onDrop(filesVal) { + const { files } = this.state; + const filesLimit = 1; + let oldFiles = files; + oldFiles = oldFiles.concat(filesVal); + if (oldFiles.length > filesLimit) { + console.log('Cannot upload more than ' + filesLimit + ' items.'); + } else { + this.setState({ img: filesVal[0] }); + } + } + + sendValues = (values) => { + const { submit } = this.props; + const { img } = this.state; + const { avatarInit } = this.props; + const avatar = img === null ? avatarInit : img; + setTimeout(() => { + submit(values, avatar); + this.setState({ img: null }); + }, 500); + } + + render() { + const { + classes, + openForm, + closeForm, + avatarInit, + addContact + } = this.props; + const { img } = this.state; + const branch = ''; + return ( + <div> + <Tooltip title="Add New Contact"> + <Fab color="secondary" onClick={() => addContact()} className={classes.addBtn}> + <Add /> + </Fab> + </Tooltip> + <FloatingPanel openForm={openForm} branch={branch} closeForm={closeForm}> + <AddContactForm + onSubmit={this.sendValues} + onDrop={this.onDrop} + imgAvatar={img === null ? avatarInit : img} + /> + </FloatingPanel> + </div> + ); + } +} + +AddContact.propTypes = { + classes: PropTypes.object.isRequired, + submit: PropTypes.func.isRequired, + addContact: PropTypes.func.isRequired, + openForm: PropTypes.bool.isRequired, + avatarInit: PropTypes.string.isRequired, + closeForm: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(AddContact); diff --git a/front/odiparpack/app/components/Contact/AddContactForm.js b/front/odiparpack/app/components/Contact/AddContactForm.js new file mode 100644 index 0000000..0b51684 --- /dev/null +++ b/front/odiparpack/app/components/Contact/AddContactForm.js @@ -0,0 +1,273 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Dropzone from 'react-dropzone'; +import { withStyles } from '@material-ui/core/styles'; +import Type from 'ba-styles/Typography.scss'; +import PhotoCamera from '@material-ui/icons/PhotoCamera'; +import { connect } from 'react-redux'; +import { reduxForm, Field } from 'redux-form/immutable'; +import PermContactCalendar from '@material-ui/icons/PermContactCalendar'; +import Bookmark from '@material-ui/icons/Bookmark'; +import LocalPhone from '@material-ui/icons/LocalPhone'; +import Email from '@material-ui/icons/Email'; +import Smartphone from '@material-ui/icons/Smartphone'; +import LocationOn from '@material-ui/icons/LocationOn'; +import Work from '@material-ui/icons/Work'; +import Language from '@material-ui/icons/Language'; +import css from 'ba-styles/Form.scss'; +import { Button, Avatar, IconButton, Typography, Tooltip, InputAdornment } from '@material-ui/core'; +import { TextFieldRedux } from '../Forms/ReduxFormMUI'; +import styles from './contact-jss'; + + +// validation functions +const required = value => (value == null ? 'Required' : undefined); +const email = value => ( + value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) + ? 'Invalid email' + : undefined +); + +class AddContactForm extends React.Component { + saveRef = ref => { + this.ref = ref; + return this.ref; + }; + + render() { + const { + classes, + reset, + pristine, + submitting, + handleSubmit, + onDrop, + imgAvatar + } = this.props; + let dropzoneRef; + const acceptedFiles = ['image/jpeg', 'image/png', 'image/bmp']; + const fileSizeLimit = 300000; + const imgPreview = img => { + if (typeof img !== 'string' && img !== '') { + return URL.createObjectURL(imgAvatar); + } + return img; + }; + return ( + <div> + <form onSubmit={handleSubmit}> + <section className={css.bodyForm}> + <div> + <Typography variant="button" component="p" className={Type.textCenter}>Upload Avatar</Typography> + <Dropzone + className={classes.hiddenDropzone} + accept={acceptedFiles.join(',')} + acceptClassName="stripes" + onDrop={onDrop} + maxSize={fileSizeLimit} + ref={(node) => { dropzoneRef = node; }} + > + {({ getRootProps, getInputProps }) => ( + <div {...getRootProps()}> + <input {...getInputProps()} /> + </div> + )} + </Dropzone> + <div className={classes.avatarWrap}> + <Avatar + alt="John Doe" + className={classes.uploadAvatar} + src={imgPreview(imgAvatar)} + /> + <Tooltip id="tooltip-upload" title="Upload Photo"> + <IconButton + className={classes.buttonUpload} + component="button" + onClick={() => { + dropzoneRef.open(); + }} + > + <PhotoCamera /> + </IconButton> + </Tooltip> + </div> + </div> + <div> + <Field + name="name" + component={TextFieldRedux} + placeholder="Name" + label="Name" + validate={required} + required + ref={this.saveRef} + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <PermContactCalendar /> + </InputAdornment> + ) + }} + /> + </div> + <div> + <Field + name="title" + component={TextFieldRedux} + placeholder="Title" + label="Title" + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <Bookmark /> + </InputAdornment> + ) + }} + /> + </div> + <div> + <Field + name="phone" + component={TextFieldRedux} + placeholder="Phone" + type="tel" + label="Phone" + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <LocalPhone /> + </InputAdornment> + ) + }} + /> + </div> + <div> + <Field + name="secondaryPhone" + component={TextFieldRedux} + placeholder="Secondary Phone" + type="tel" + label="Secondary Phone" + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <Smartphone /> + </InputAdornment> + ) + }} + /> + </div> + <div> + <Field + name="personalEmail" + component={TextFieldRedux} + placeholder="Personal Email" + type="email" + validate={email} + label="Personal Email" + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <Email /> + </InputAdornment> + ) + }} + /> + </div> + <div> + <Field + name="companyEmail" + component={TextFieldRedux} + placeholder="Company Email" + type="email" + validate={email} + label="Company Email" + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <Work /> + </InputAdornment> + ) + }} + /> + </div> + <div> + <Field + name="address" + component={TextFieldRedux} + placeholder="Address" + label="Address" + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <LocationOn /> + </InputAdornment> + ) + }} + /> + </div> + <div> + <Field + name="website" + component={TextFieldRedux} + placeholder="Website" + type="url" + label="Website" + className={classes.field} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + <Language /> + </InputAdornment> + ) + }} + /> + </div> + </section> + <div className={css.buttonArea}> + <Button variant="contained" color="secondary" type="submit" disabled={submitting}> + Submit + </Button> + <Button + type="button" + disabled={pristine || submitting} + onClick={reset} + > + Reset + </Button> + </div> + </form> + </div> + ); + } +} + +AddContactForm.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + onDrop: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, + imgAvatar: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, +}; + +const AddContactFormRedux = reduxForm({ + form: 'immutableAddContact', + enableReinitialize: true, +})(AddContactForm); + +const AddContactInit = connect( + state => ({ + initialValues: state.getIn(['contact', 'formValues']) + }) +)(AddContactFormRedux); + +export default withStyles(styles)(AddContactInit); diff --git a/front/odiparpack/app/components/Contact/ContactDetail.js b/front/odiparpack/app/components/Contact/ContactDetail.js new file mode 100644 index 0000000..f6d2dfc --- /dev/null +++ b/front/odiparpack/app/components/Contact/ContactDetail.js @@ -0,0 +1,195 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import Edit from '@material-ui/icons/Edit'; +import Star from '@material-ui/icons/Star'; +import StarBorder from '@material-ui/icons/StarBorder'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import LocalPhone from '@material-ui/icons/LocalPhone'; +import Email from '@material-ui/icons/Email'; +import Smartphone from '@material-ui/icons/Smartphone'; +import LocationOn from '@material-ui/icons/LocationOn'; +import Work from '@material-ui/icons/Work'; +import Language from '@material-ui/icons/Language'; +import { + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, + Menu, + MenuItem, + IconButton, + Typography, + Divider, +} from '@material-ui/core'; +import styles from './contact-jss'; + + +const optionsOpt = [ + 'Block Contact', + 'Delete Contact', + 'Option 1', + 'Option 2', + 'Option 3', +]; + +const ITEM_HEIGHT = 48; + +class ContactDetail extends React.Component { + state = { + anchorElOpt: null, + }; + + handleClickOpt = event => { + this.setState({ anchorElOpt: event.currentTarget }); + }; + + handleCloseOpt = () => { + this.setState({ anchorElOpt: null }); + }; + + deleteContact = (item) => { + this.props.remove(item); + this.setState({ anchorElOpt: null }); + } + + render() { + const { + classes, + dataContact, + itemSelected, + edit, + favorite, + showMobileDetail + } = this.props; + const { anchorElOpt } = this.state; + return ( + <main className={classNames(classes.content, showMobileDetail ? classes.detailPopup : '')}> + <section className={classes.cover}> + <div className={classes.opt}> + <IconButton className={classes.favorite} aria-label="Favorite" onClick={() => favorite(dataContact.get(itemSelected))}> + {dataContact.getIn([itemSelected, 'favorited']) ? (<Star />) : <StarBorder />} + </IconButton> + <IconButton aria-label="Edit" onClick={() => edit(dataContact.get(itemSelected))}> + <Edit /> + </IconButton> + <IconButton + aria-label="More" + aria-owns={anchorElOpt ? 'long-menu' : null} + aria-haspopup="true" + className={classes.button} + onClick={this.handleClickOpt} + > + <MoreVertIcon /> + </IconButton> + <Menu + id="long-menu" + anchorEl={anchorElOpt} + open={Boolean(anchorElOpt)} + onClose={this.handleCloseOpt} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: 200, + }, + }} + > + {optionsOpt.map(option => { + if (option === 'Delete Contact') { + return ( + <MenuItem key={option} selected={option === 'Edit Profile'} onClick={() => this.deleteContact(dataContact.get(itemSelected))}> + {option} + </MenuItem> + ); + } + return ( + <MenuItem key={option} selected={option === 'Edit Profile'} onClick={this.handleCloseOpt}> + {option} + </MenuItem> + ); + })} + </Menu> + </div> + <Avatar alt={dataContact.getIn([itemSelected, 'name'])} src={dataContact.getIn([itemSelected, 'avatar'])} className={classes.avatar} /> + <Typography className={classes.userName} variant="h4"> + {dataContact.getIn([itemSelected, 'name'])} + <Typography variant="caption" display="block"> + {dataContact.getIn([itemSelected, 'title'])} + </Typography> + </Typography> + </section> + <div> + <List> + <ListItem> + <ListItemAvatar> + <Avatar className={classes.blueIcon}> + <LocalPhone /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dataContact.getIn([itemSelected, 'phone'])} secondary="Phone" /> + </ListItem> + <Divider variant="inset" /> + <ListItem> + <ListItemAvatar> + <Avatar className={classes.amberIcon}> + <Smartphone /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dataContact.getIn([itemSelected, 'secondaryPhone'])} secondary="Secondary Phone" /> + </ListItem> + <Divider variant="inset" /> + <ListItem> + <ListItemAvatar> + <Avatar className={classes.tealIcon}> + <Email /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dataContact.getIn([itemSelected, 'personalEmail'])} secondary="Personal Email" /> + </ListItem> + <Divider variant="inset" /> + <ListItem> + <ListItemAvatar> + <Avatar className={classes.brownIcon}> + <Work /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dataContact.getIn([itemSelected, 'companyEmail'])} secondary="Company Email" /> + </ListItem> + <Divider variant="inset" /> + <ListItem> + <ListItemAvatar> + <Avatar className={classes.redIcon}> + <LocationOn /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dataContact.getIn([itemSelected, 'address'])} secondary="Address" /> + </ListItem> + <Divider variant="inset" /> + <ListItem> + <ListItemAvatar> + <Avatar className={classes.purpleIcon}> + <Language /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dataContact.getIn([itemSelected, 'website'])} secondary="Website" /> + </ListItem> + </List> + </div> + </main> + ); + } +} + +ContactDetail.propTypes = { + classes: PropTypes.object.isRequired, + showMobileDetail: PropTypes.bool.isRequired, + dataContact: PropTypes.object.isRequired, + itemSelected: PropTypes.number.isRequired, + edit: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + favorite: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(ContactDetail); diff --git a/front/odiparpack/app/components/Contact/ContactHeader.js b/front/odiparpack/app/components/Contact/ContactHeader.js new file mode 100644 index 0000000..f17a1a4 --- /dev/null +++ b/front/odiparpack/app/components/Contact/ContactHeader.js @@ -0,0 +1,62 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import ArrowBack from '@material-ui/icons/ArrowBack'; +import PermContactCalendar from '@material-ui/icons/PermContactCalendar'; +import Add from '@material-ui/icons/Add'; +import { AppBar, Toolbar, Typography, Button, IconButton } from '@material-ui/core'; +import styles from './contact-jss'; + + +class ContactHeader extends React.Component { + render() { + const { + classes, + addContact, + total, + hideDetail, + showMobileDetail + } = this.props; + return ( + <AppBar + position="absolute" + className={classes.appBar} + > + <Toolbar> + {showMobileDetail && ( + <IconButton + color="inherit" + aria-label="open drawer" + onClick={() => hideDetail()} + className={classes.navIconHide} + > + <ArrowBack /> + </IconButton> + )} + <Typography variant="subtitle1" className={classes.title} color="inherit" noWrap> + <PermContactCalendar /> + {' '} +Contacts ( + {total} +) + </Typography> + <Button onClick={() => addContact()} variant="outlined" color="inherit" className={classes.button}> + <Add /> + {' '} +Add New + </Button> + </Toolbar> + </AppBar> + ); + } +} + +ContactHeader.propTypes = { + classes: PropTypes.object.isRequired, + showMobileDetail: PropTypes.bool.isRequired, + addContact: PropTypes.func.isRequired, + hideDetail: PropTypes.func.isRequired, + total: PropTypes.number.isRequired, +}; + +export default withStyles(styles)(ContactHeader); diff --git a/front/odiparpack/app/components/Contact/ContactList.js b/front/odiparpack/app/components/Contact/ContactList.js new file mode 100644 index 0000000..545687d --- /dev/null +++ b/front/odiparpack/app/components/Contact/ContactList.js @@ -0,0 +1,112 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import SearchIcon from '@material-ui/icons/Search'; +import PermContactCalendar from '@material-ui/icons/PermContactCalendar'; +import Star from '@material-ui/icons/Star'; +import { + Drawer, + Divider, + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, + BottomNavigation, + BottomNavigationAction, +} from '@material-ui/core'; +import styles from './contact-jss'; + + +class ContactList extends React.Component { + state = { + filter: 'all', + }; + + handleChange = (event, value) => { + this.setState({ filter: value }); + }; + + render() { + const { + classes, + dataContact, + itemSelected, + showDetail, + search, + keyword, + clippedRight + } = this.props; + const { filter } = this.state; + const favoriteData = dataContact.filter(item => item.get('favorited') === true); + const getItem = dataArray => dataArray.map(data => { + const index = dataContact.indexOf(data); + if (data.get('name').toLowerCase().indexOf(keyword) === -1) { + return false; + } + return ( + <ListItem + button + key={data.get('id')} + className={index === itemSelected ? classes.selected : ''} + onClick={() => showDetail(data)} + > + <ListItemAvatar> + <Avatar alt={data.get('name')} src={data.get('avatar')} className={classes.avatar} /> + </ListItemAvatar> + <ListItemText primary={data.get('name')} secondary={data.get('title')} /> + </ListItem> + ); + }); + return ( + <Fragment> + <Drawer + variant="permanent" + anchor="left" + open + classes={{ + paper: classes.drawerPaper, + }} + > + <div> + <div className={classNames(classes.toolbar, clippedRight && classes.clippedRight)}> + <div className={classes.flex}> + <div className={classes.searchWrapper}> + <div className={classes.search}> + <SearchIcon /> + </div> + <input className={classes.input} onChange={(event) => search(event)} placeholder="Search Contact" /> + </div> + </div> + </div> + <Divider /> + <List> + {filter === 'all' ? getItem(dataContact) : getItem(favoriteData)} + </List> + </div> + </Drawer> + <BottomNavigation value={filter} onChange={this.handleChange} className={classes.bottomFilter}> + <BottomNavigationAction label="All" value="all" icon={<PermContactCalendar />} /> + <BottomNavigationAction label="Favorites" value="favorites" icon={<Star />} /> + </BottomNavigation> + </Fragment> + ); + } +} + +ContactList.propTypes = { + classes: PropTypes.object.isRequired, + dataContact: PropTypes.object.isRequired, + keyword: PropTypes.string.isRequired, + itemSelected: PropTypes.number.isRequired, + showDetail: PropTypes.func.isRequired, + search: PropTypes.func.isRequired, + clippedRight: PropTypes.bool, +}; + +ContactList.defaultProps = { + clippedRight: false +}; + +export default withStyles(styles)(ContactList); diff --git a/front/odiparpack/app/components/Contact/contact-jss.js b/front/odiparpack/app/components/Contact/contact-jss.js new file mode 100644 index 0000000..6745527 --- /dev/null +++ b/front/odiparpack/app/components/Contact/contact-jss.js @@ -0,0 +1,282 @@ +import { amber, blue, deepPurple as purple, teal, brown, red } from '@material-ui/core/colors'; + +const drawerWidth = 240; +const drawerHeight = 630; + +const styles = theme => ({ + root: { + flexGrow: 1, + height: drawerHeight, + zIndex: 1, + overflow: 'hidden', + position: 'relative', + [theme.breakpoints.up('sm')]: { + display: 'flex', + }, + borderRadius: 2, + boxShadow: theme.shadows[2] + }, + addBtn: { + position: 'fixed', + bottom: 30, + right: 30, + zIndex: 100 + }, + appBar: { + zIndex: theme.zIndex.drawer + 1, + background: theme.palette.secondary.main, + height: 64, + display: 'flex', + justifyContent: 'center', + '& $avatar': { + marginRight: 10 + }, + '& h2': { + flex: 1 + }, + '& $button': { + color: theme.palette.common.white + } + }, + button: { + [theme.breakpoints.down('sm')]: { + display: 'none' + }, + }, + online: { + background: '#CDDC39' + }, + bussy: { + background: '#EF5350' + }, + idle: { + background: '#FFC107' + }, + offline: { + background: '#9E9E9E' + }, + status: { + padding: '2px 6px', + '& span': { + borderRadius: '50%', + display: 'inline-block', + marginRight: 2, + width: 10, + height: 10, + border: `1px solid ${theme.palette.common.white}` + } + }, + appBarShift: { + marginLeft: 0, + width: '100%', + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + [theme.breakpoints.up('md')]: { + marginLeft: drawerWidth, + width: `calc(100% - ${drawerWidth}px)`, + }, + }, + drawerPaper: { + [theme.breakpoints.up('sm')]: { + width: drawerWidth, + }, + position: 'relative', + paddingBottom: 65, + height: drawerHeight, + }, + clippedRight: {}, + toolbar: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: 8, + position: 'relative', + '&$clippedRight': { + marginTop: 66 + } + }, + content: { + flexGrow: 1, + paddingTop: 64, + backgroundColor: theme.palette.background.paper, + }, + detailPopup: { + [theme.breakpoints.down('xs')]: { + position: 'absolute', + top: 0, + left: 0, + zIndex: 1200, + width: '100%', + overflow: 'auto', + height: 'calc(100% - 50px)' + } + }, + title: { + display: 'flex', + flex: 1, + '& svg': { + marginRight: 5 + } + }, + flex: { + flex: 1, + }, + searchWrapper: { + fontFamily: theme.typography.fontFamily, + position: 'relative', + borderRadius: 2, + display: 'block', + background: theme.palette.grey[100] + }, + search: { + width: 'auto', + height: '100%', + top: 0, + left: 20, + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + input: { + font: 'inherit', + padding: `${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(6)}px`, + border: 0, + display: 'block', + verticalAlign: 'middle', + whiteSpace: 'normal', + background: 'none', + margin: 0, // Reset for Safari + color: 'inherit', + width: '100%', + '&:focus': { + outline: 0, + }, + }, + bottomFilter: { + position: 'absolute', + width: '100%', + [theme.breakpoints.up('sm')]: { + width: 240, + }, + zIndex: 2000, + bottom: 0, + left: 0, + background: theme.palette.grey[100], + borderTop: `1px solid ${theme.palette.grey[300]}`, + borderRight: `1px solid ${theme.palette.grey[300]}`, + }, + avatar: {}, + userName: { + textAlign: 'left' + }, + cover: { + padding: 20, + height: 130, + position: 'relative', + background: theme.palette.primary.light, + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + '& $avatar': { + boxShadow: theme.shadows[4], + width: 100, + height: 100, + marginRight: 20 + }, + }, + opt: { + position: 'absolute', + top: 10, + right: 10, + }, + favorite: { + color: amber[500] + }, + redIcon: { + background: red[50], + '& svg': { + color: red[500] + } + }, + brownIcon: { + background: brown[50], + '& svg': { + color: brown[500] + } + }, + tealIcon: { + background: teal[50], + '& svg': { + color: teal[500] + } + }, + blueIcon: { + background: blue[50], + '& svg': { + color: blue[500] + } + }, + amberIcon: { + background: amber[50], + '& svg': { + color: amber[500] + } + }, + purpleIcon: { + background: purple[50], + '& svg': { + color: purple[500] + } + }, + field: { + width: '100%', + marginBottom: 20, + '& svg': { + color: theme.palette.grey[400], + fontSize: 18, + } + }, + uploadAvatar: { + width: '100%', + height: '100%', + background: theme.palette.grey[200], + boxShadow: theme.shadows[4], + }, + selected: { + background: theme.palette.secondary.light, + borderLeft: `2px solid ${theme.palette.secondary.main}`, + paddingLeft: 22, + '& h3': { + color: theme.palette.secondary.dark + } + }, + hiddenDropzone: { + display: 'none' + }, + avatarWrap: { + width: 100, + height: 100, + margin: '10px auto 30px', + position: 'relative' + }, + buttonUpload: { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)' + }, + navIconHide: { + marginRight: theme.spacing(1), + [theme.breakpoints.up('sm')]: { + display: 'none' + } + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Counter/CounterWidget.js b/front/odiparpack/app/components/Counter/CounterWidget.js new file mode 100644 index 0000000..f22bfb8 --- /dev/null +++ b/front/odiparpack/app/components/Counter/CounterWidget.js @@ -0,0 +1,80 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import CountUp from 'react-countup'; +import { withStyles } from '@material-ui/core/styles'; +import { Typography, Paper } from '@material-ui/core'; + +const styles = theme => ({ + root: { + flexGrow: 1, + justifyContent: 'space-between', + alignItems: 'flex-start', + padding: 10, + height: 190, + marginBottom: 6, + display: 'flex', + [theme.breakpoints.up('sm')]: { + height: 120, + marginBottom: -1, + alignItems: 'flex-end', + }, + [theme.breakpoints.down('xs')]: { + flexDirection: 'column', + }, + '& > *': { + padding: '0 5px' + } + }, + title: { + color: theme.palette.common.white, + fontSize: 16, + fontWeight: 400 + }, + counter: { + color: theme.palette.common.white, + fontSize: 28, + fontWeight: 500 + }, + customContent: { + textAlign: 'right' + } +}); + +class CounterWidget extends PureComponent { + render() { + const { + classes, + color, + start, + end, + duration, + title, + children + } = this.props; + return ( + <Paper className={classes.root} style={{ backgroundColor: color }}> + <div> + <Typography className={classes.counter}> + <CountUp start={start} end={end} duration={duration} useEasing /> + </Typography> + <Typography className={classes.title} variant="subtitle1">{title}</Typography> + </div> + <div className={classes.customContent}> + {children} + </div> + </Paper> + ); + } +} + +CounterWidget.propTypes = { + classes: PropTypes.object.isRequired, + color: PropTypes.string.isRequired, + start: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + duration: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +export default withStyles(styles)(CounterWidget); diff --git a/front/odiparpack/app/components/Divider/divider-jss.js b/front/odiparpack/app/components/Divider/divider-jss.js new file mode 100644 index 0000000..e813b8d --- /dev/null +++ b/front/odiparpack/app/components/Divider/divider-jss.js @@ -0,0 +1,70 @@ +const space = { + margin: '40px 0' +}; +const styles = theme => ({ + gradient: { + extend: space, + border: 0, + height: 1, + background: '#333', + backgroundImage: 'linear-gradient(to right, #fff, #8c8c8c, #fff)' + }, + colorDash: { + border: 0, + extend: space, + borderBottom: `1px dashed ${theme.palette.grey[100]}`, + background: '#999' + }, + shadow: { + height: 12, + extend: space, + border: 0, + boxShadow: 'inset 0 12px 12px -12px rgba(0, 0, 0, 0.5)' + }, + inset: { + border: 0, + extend: space, + height: 0, + borderTop: '1px solid rgba(0, 0, 0, 0.1)', + borderBottom: '1px solid rgba(255, 255, 255, 0.3)' + }, + flairedEdges: { + overflow: 'visible', /* For IE */ + extend: space, + height: 30, + borderStyle: 'solid', + borderColor: theme.palette.grey[400], + borderWidth: '1px 0 0 0', + borderRadius: 20, + '&:before': { + display: 'block', + content: '""', + height: 30, + marginTop: -31, + borderStyle: 'solid', + borderColor: theme.palette.grey[400], + borderWidth: '0 0 1px 0', + borderRadius: 20 + } + }, + content: { + overflow: 'visible', /* For IE */ + extend: space, + padding: 0, + border: 'none', + borderTop: `1px solid ${theme.palette.grey[400]}`, + color: '#333', + textAlign: 'center', + '&:after': { + content: 'attr(data-content)', + display: 'inline-block', + position: 'relative', + top: -15, + fontSize: 14, + padding: '0 0.25em', + background: '#FFF' + } + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Divider/index.js b/front/odiparpack/app/components/Divider/index.js new file mode 100644 index 0000000..8e5c010 --- /dev/null +++ b/front/odiparpack/app/components/Divider/index.js @@ -0,0 +1,148 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import styles from './divider-jss'; + +/* Gradient Divider */ +const Gradient = props => { + const { + thin, + classes, + ...rest + } = props; + return ( + <hr className={classes.gradient} style={{ height: `${thin}` }} {...rest} /> + ); +}; + +Gradient.propTypes = { + thin: PropTypes.number, + classes: PropTypes.object.isRequired, +}; + +Gradient.defaultProps = { + thin: 1 +}; + +export const GradientDivider = withStyles(styles)(Gradient); + +/* Dash Divider */ + +const Dash = props => { + const { + thin, + classes, + ...rest + } = props; + return ( + <hr className={classes.colorDash} style={{ height: `${thin}` }} {...rest} /> + ); +}; + +Dash.propTypes = { + classes: PropTypes.object.isRequired, + thin: PropTypes.number, +}; + +Dash.defaultProps = { + thin: 1 +}; + +export const DashDivider = withStyles(styles)(Dash); + +/* Shadow Divider */ + +const Shadow = props => { + const { + classes, + thin, + ...rest + } = props; + return ( + <hr className={classes.shadow} style={{ height: `${thin}` }} {...rest} /> + ); +}; + +Shadow.propTypes = { + classes: PropTypes.object.isRequired, + thin: PropTypes.number, +}; + +Shadow.defaultProps = { + thin: 1 +}; + +export const ShadowDivider = withStyles(styles)(Shadow); + +/* Shadow Inset */ + +const Inset = props => { + const { + classes, + thin, + ...rest + } = props; + return ( + <hr className={classes.inset} style={{ height: `${thin}` }} {...rest} /> + ); +}; + +Inset.propTypes = { + classes: PropTypes.object.isRequired, + thin: PropTypes.number, +}; + +Inset.defaultProps = { + thin: 1 +}; + +export const InsetDivider = withStyles(styles)(Inset); + +/* Shadow FlairedEdges */ + +export const FlairedEdges = props => { + const { + classes, + thin, + ...rest + } = props; + return ( + <hr className={classes.flairedEdges} style={{ height: `${thin}` }} {...rest} /> + ); +}; + +FlairedEdges.propTypes = { + classes: PropTypes.object.isRequired, + thin: PropTypes.number, +}; + +FlairedEdges.defaultProps = { + thin: 1 +}; + +export const FlairedEdgesDivider = withStyles(styles)(FlairedEdges); + + +export const Content = props => { + const { + classes, + thin, + content, + ...rest + } = props; + return ( + <hr className={classes.content} style={{ height: `${thin}` }} data-content={content} {...rest} /> + ); +}; + +Content.propTypes = { + classes: PropTypes.object.isRequired, + thin: PropTypes.number, + content: PropTypes.string.isRequired, +}; + +Content.defaultProps = { + thin: 1 +}; + +export const ContentDivider = withStyles(styles)(Content); diff --git a/front/odiparpack/app/components/Email/ComposeEmail.js b/front/odiparpack/app/components/Email/ComposeEmail.js new file mode 100644 index 0000000..8d06ebd --- /dev/null +++ b/front/odiparpack/app/components/Email/ComposeEmail.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Add from '@material-ui/icons/Add'; +import { Fab, Tooltip } from '@material-ui/core'; +import ComposeEmailForm from './ComposeEmailForm'; +import FloatingPanel from '../Panel/FloatingPanel'; +import styles from './email-jss'; + + +class ComposeEmail extends React.Component { + render() { + const { + classes, + open, + closeForm, + sendEmail, + to, + subject, + validMail, + inputChange, + compose + } = this.props; + const branch = ''; + return ( + <div> + <Tooltip title="Compose Email"> + <Fab color="secondary" onClick={() => compose()} className={classes.addBtn}> + <Add /> + </Fab> + </Tooltip> + <FloatingPanel + openForm={open} + branch={branch} + closeForm={closeForm} + title="Compose Email" + extraSize + > + <ComposeEmailForm + to={to} + subject={subject} + validMail={validMail} + sendEmail={sendEmail} + closeForm={closeForm} + inputChange={inputChange} + /> + </FloatingPanel> + </div> + ); + } +} + +ComposeEmail.propTypes = { + classes: PropTypes.object.isRequired, + open: PropTypes.bool.isRequired, + to: PropTypes.string.isRequired, + subject: PropTypes.string.isRequired, + validMail: PropTypes.string.isRequired, + compose: PropTypes.func.isRequired, + closeForm: PropTypes.func.isRequired, + sendEmail: PropTypes.func.isRequired, + inputChange: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(ComposeEmail); diff --git a/front/odiparpack/app/components/Email/ComposeEmailForm.js b/front/odiparpack/app/components/Email/ComposeEmailForm.js new file mode 100644 index 0000000..3620d4d --- /dev/null +++ b/front/odiparpack/app/components/Email/ComposeEmailForm.js @@ -0,0 +1,262 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Editor } from 'react-draft-wysiwyg'; +import { convertFromRaw, EditorState, convertToRaw } from 'draft-js'; +import draftToHtml from 'draftjs-to-html'; +import Dropzone from 'react-dropzone'; +import { withStyles } from '@material-ui/core/styles'; +import TextField from '@material-ui/core/TextField'; +import Attachment from '@material-ui/icons/Attachment'; +import FileIcon from '@material-ui/icons/Description'; +import ActionDelete from '@material-ui/icons/Delete'; +import Send from '@material-ui/icons/Send'; +import EditorStyle from 'ba-styles/TextEditor.scss'; +import css from 'ba-styles/Form.scss'; +import 'ba-styles/vendors/react-draft-wysiwyg/react-draft-wysiwyg.css'; +import { Button, Grid, Typography, Snackbar, IconButton } from '@material-ui/core'; +import isImage from '../Forms/helpers/helpers.js'; +import styles from './email-jss'; + + +const content = { + blocks: [{ + key: '637gr', + text: 'Lorem ipsum dolor sit amet 😀', + type: 'unstyled', + depth: 0, + inlineStyleRanges: [], + entityRanges: [], + data: {} + }], + entityMap: {} +}; + +class ComposeEmailForm extends React.Component { + constructor(props) { + super(props); + const contentBlock = convertFromRaw(content); + if (contentBlock) { + const editorState = EditorState.createWithContent(contentBlock); + this.state = { + openSnackBar: false, + errorMessage: '', + files: [], + editorState, + emailContent: draftToHtml(convertToRaw(editorState.getCurrentContent())), + }; + } + this.onDrop = this.onDrop.bind(this); + } + + onDrop(filesVal) { + const { files } = this.state; + let oldFiles = files; + const filesLimit = 3; + oldFiles = oldFiles.concat(filesVal); + if (oldFiles.length > filesLimit) { + console.log('Cannot upload more than ' + filesLimit + ' items.'); + } else { + this.setState({ files: oldFiles }); + } + } + + onEditorStateChange = editorState => { + this.setState({ + editorState, + emailContent: draftToHtml(convertToRaw(editorState.getCurrentContent())) + }); + }; + + onDropRejected() { + this.setState({ + openSnackBar: true, + errorMessage: 'File too big, max size is 3MB', + }); + } + + handleRequestCloseSnackBar = () => { + this.setState({ + openSnackBar: false, + }); + }; + + handleRemove(file, fileIndex) { + const thisFiles = this.state.files; + // This is to prevent memory leaks. + window.URL.revokeObjectURL(file.preview); + + thisFiles.splice(fileIndex, 1); + this.setState({ files: thisFiles }); + } + + handleSend = (to, subject, emailContent, files) => { + this.props.sendEmail(to, subject, emailContent, files); + this.setState({ emailContent: '', files: [] }); + }; + + render() { + const { + classes, + closeForm, + to, + subject, + validMail, + inputChange + } = this.props; + const { + editorState, + emailContent, + files, + openSnackBar, + errorMessage, + } = this.state; + let dropzoneRef; + const deleteBtn = (file, index) => ( + <div className="middle"> + <IconButton onClick={() => this.handleRemove(file, index)}> + <ActionDelete className="removeBtn" /> + </IconButton> + </div> + ); + const previews = filesArray => filesArray.map((file, index) => { + if (isImage(file)) { + const base64Img = URL.createObjectURL(file); + return ( + <div key={index.toString()} className={classes.item}> + <div className="imageContainer col fileIconImg"> + <figure className="imgWrap"><img className="smallPreviewImg" src={base64Img} alt="preview" /></figure> + {deleteBtn(file, index)} + </div> + <Typography noWrap variant="caption">{file.name}</Typography> + </div> + ); + } + return ( + <div key={index.toString()} className={classes.item}> + <div className="imageContainer col fileIconImg"> + <div className="fileWrap"> + <FileIcon className="smallPreviewImg" alt="preview" /> + {deleteBtn(file, index)} + </div> + </div> + <Typography noWrap variant="caption">{file.name}</Typography> + </div> + ); + }); + const fileSizeLimit = 3000000; + return ( + <div> + <form> + <section className={css.bodyForm}> + <div> + <TextField + error={validMail === 'Invalid email'} + id="to" + label="To" + helperText={validMail} + className={classes.field} + type="email" + placeholder="To" + value={to} + onChange={(event) => inputChange(event, 'to')} + margin="normal" + /> + </div> + <div> + <TextField + id="subject" + label="Subject" + className={classes.field} + placeholder="Subject" + value={subject} + onChange={(event) => inputChange(event, 'subject')} + margin="normal" + /> + </div> + <Grid container alignItems="center"> + <Dropzone + className={classes.hiddenDropzone} + acceptClassName="stripes" + onDrop={this.onDrop} + maxSize={fileSizeLimit} + ref={(node) => { dropzoneRef = node; }} + > + {({ getRootProps, getInputProps }) => ( + <div {...getRootProps()}> + <input {...getInputProps()} /> + </div> + )} + </Dropzone> + <Button + className={classes.buttonUpload} + color="secondary" + component="button" + onClick={() => { + dropzoneRef.open(); + }} + > + <Attachment /> + Attach Files + </Button> + + <Typography variant="caption">(Max 3MB)</Typography> + </Grid> + <div className={classes.preview}> + {previews(files)} + </div> + <div> + <Editor + editorState={editorState} + editorClassName={EditorStyle.TextEditor} + toolbarClassName={EditorStyle.ToolbarEditor} + onEditorStateChange={this.onEditorStateChange} + toolbar={{ + options: ['inline', 'fontSize', 'fontFamily', 'colorPicker', 'image', 'emoji', 'list', 'textAlign', 'link'], + inline: { inDropdown: true }, + color: true, + list: { inDropdown: true }, + textAlign: { inDropdown: true }, + link: { inDropdown: true }, + }} + /> + </div> + </section> + <div className={css.buttonArea}> + <Button type="button" onClick={() => closeForm()}> + Discard + </Button> + <Button + variant="contained" + color="secondary" + type="button" + disabled={!to || !subject} + onClick={() => this.handleSend(to, subject, emailContent, files)} + > + Send + {' '} + <Send className={classes.sendIcon} /> + </Button> + </div> + </form> + <Snackbar + open={openSnackBar} + message={errorMessage} + autoHideDuration={4000} + onClose={this.handleRequestCloseSnackBar} + /> + </div> + ); + } +} + +ComposeEmailForm.propTypes = { + classes: PropTypes.object.isRequired, + to: PropTypes.string.isRequired, + subject: PropTypes.string.isRequired, + validMail: PropTypes.string.isRequired, + sendEmail: PropTypes.func.isRequired, + closeForm: PropTypes.func.isRequired, + inputChange: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(ComposeEmailForm); diff --git a/front/odiparpack/app/components/Email/EmailHeader.js b/front/odiparpack/app/components/Email/EmailHeader.js new file mode 100644 index 0000000..eceb757 --- /dev/null +++ b/front/odiparpack/app/components/Email/EmailHeader.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import SearchIcon from '@material-ui/icons/Search'; +import MenuIcon from '@material-ui/icons/Menu'; +import { AppBar, Hidden, Toolbar, Typography, IconButton } from '@material-ui/core'; +import styles from './email-jss'; + + +class EmailHeader extends React.Component { + render() { + const { classes, search, handleDrawerToggle } = this.props; + return ( + <AppBar position="absolute" className={classes.appBar}> + <Toolbar> + <Hidden smDown> + <Typography variant="h6" color="inherit" className={classes.title} noWrap> + Email + </Typography> + </Hidden> + <IconButton + color="inherit" + aria-label="open drawer" + onClick={() => handleDrawerToggle()} + className={classes.navIconHide} + > + <MenuIcon /> + </IconButton> + <div className={classes.flex}> + <div className={classes.wrapper}> + <div className={classes.search}> + <SearchIcon /> + </div> + <input className={classes.input} onChange={(event) => search(event)} placeholder="Search Email" /> + </div> + </div> + </Toolbar> + </AppBar> + ); + } +} + +EmailHeader.propTypes = { + classes: PropTypes.object.isRequired, + search: PropTypes.func.isRequired, + handleDrawerToggle: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(EmailHeader); diff --git a/front/odiparpack/app/components/Email/EmailList.js b/front/odiparpack/app/components/Email/EmailList.js new file mode 100644 index 0000000..1fc3507 --- /dev/null +++ b/front/odiparpack/app/components/Email/EmailList.js @@ -0,0 +1,314 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Bookmark from '@material-ui/icons/Bookmark'; +import Delete from '@material-ui/icons/Delete'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import classNames from 'classnames'; +import Flag from '@material-ui/icons/Flag'; +import People from '@material-ui/icons/People'; +import QuestionAnswer from '@material-ui/icons/QuestionAnswer'; +import ReportIcon from '@material-ui/icons/Report'; +import LabelIcon from '@material-ui/icons/Label'; +import FileIcon from '@material-ui/icons/Description'; +import Download from '@material-ui/icons/CloudDownload'; +import StarBorder from '@material-ui/icons/StarBorder'; +import Star from '@material-ui/icons/Star'; +import { + List, + Typography, + ExpansionPanel, + ExpansionPanelDetails, + ExpansionPanelSummary, + ExpansionPanelActions, + Tooltip, + IconButton, + Avatar, + Button, + ListSubheader, + Menu, + MenuItem, + Divider, +} from '@material-ui/core'; +import isImage from '../Forms/helpers/helpers.js'; +import styles from './email-jss'; + + +const ITEM_HEIGHT = 80; +class EmailList extends React.Component { + state = { + anchorElOpt: null, + itemToMove: null + }; + + handleClickOpt = (event, item) => { + this.setState({ + anchorElOpt: event.currentTarget, + itemToMove: item + }); + }; + + handleCloseOpt = () => { + this.setState({ anchorElOpt: null }); + }; + + handleMoveTo = (item, category) => { + this.props.moveTo(item, category); + this.setState({ anchorElOpt: null }); + } + + render() { + const { + classes, + emailData, + openMail, + filterPage, + keyword, + remove, + toggleStar, + reply + } = this.props; + const { anchorElOpt, itemToMove } = this.state; + /* Basic Filter */ + const inbox = emailData.filter(item => item.get('category') !== 'sent' && item.get('category') !== 'spam'); + const stared = emailData.filter(item => item.get('stared')); + const sent = emailData.filter(item => item.get('category') === 'sent'); + const spam = emailData.filter(item => item.get('category') === 'spam'); + /* Category Filter */ + const updates = emailData.filter(item => item.get('category') === 'updates'); + const social = emailData.filter(item => item.get('category') === 'social'); + const forums = emailData.filter(item => item.get('category') === 'forums'); + const promos = emailData.filter(item => item.get('category') === 'promos'); + const getCategory = cat => { + switch (cat) { + case 'updates': + return ( + <span className={classNames(classes.iconOrange, classes.category)}> + <Flag /> + {' '} +Updates + </span> + ); + case 'social': + return ( + <span className={classNames(classes.iconRed, classes.category)}> + <People /> + {' '} +Social + </span> + ); + case 'promos': + return ( + <span className={classNames(classes.iconBlue, classes.category)}> + <LabelIcon /> + {' '} +Promos + </span> + ); + case 'forums': + return ( + <span className={classNames(classes.iconCyan, classes.category)}> + <QuestionAnswer /> + {' '} +Forums + </span> + ); + default: + return false; + } + }; + const attachmentPreview = filesArray => filesArray.map((file, index) => { + const base64File = URL.createObjectURL(file); + if (isImage(file)) { + return ( + <div key={index.toString()} className={classes.item}> + <div className="imageContainer col fileIconImg"> + <div className="downloadBtn"> + <IconButton color="secondary" component="a" href={base64File} target="_blank"> + <Download /> + </IconButton> + </div> + <figure className="imgWrap"><img className="smallPreviewImg" src={base64File} alt="preview" /></figure> + </div> + <Typography noWrap>{file.name}</Typography> + </div> + ); + } + return ( + <div key={index.toString()} className={classes.item}> + <div className="imageContainer col fileIconImg"> + <div className="fileWrap"> + <div className="downloadBtn"> + <IconButton color="secondary" href={base64File} target="_blank"> + <Download /> + </IconButton> + </div> + <FileIcon className="smallPreviewImg" alt="preview" /> + </div> + </div> + <Typography noWrap>{file.name}</Typography> + </div> + ); + }); + const getEmail = dataArray => dataArray.map(mail => { + const renderHTML = { __html: mail.get('content') }; + if (mail.get('subject').toLowerCase().indexOf(keyword) === -1) { + return false; + } + return ( + <ExpansionPanel className={classes.emailList} key={mail.get('id')} onChange={() => openMail(mail)}> + <ExpansionPanelSummary className={classes.emailSummary} expandIcon={<ExpandMoreIcon />}> + <div className={classes.fromHeading}> + <Tooltip id="tooltip-mark" title="Stared"> + <IconButton onClick={() => toggleStar(mail)} className={classes.starBtn}>{mail.get('stared') ? (<Star className={classes.iconOrange} />) : (<StarBorder />) }</IconButton> + </Tooltip> + {mail.get('category') !== 'spam' + ? (<Avatar alt="avatar" src={mail.get('avatar')} className={classes.avatar} />) + : (<Avatar alt="avatar" className={classes.avatar}><ReportIcon /></Avatar>) + } + <Typography className={classes.heading}> + {mail.get('category') === 'sent' && ('To ')} + {mail.get('name')} + <Typography variant="caption" display="block">{mail.get('date')}</Typography> + </Typography> + </div> + <div className={classes.column}> + <Typography className={classes.secondaryHeading} noWrap>{mail.get('subject')}</Typography> + {getCategory(mail.get('category'))} + </div> + </ExpansionPanelSummary> + <ExpansionPanelDetails className={classes.details}> + <section> + <div className={classes.topAction}> + <Typography className={classes.headMail}> + {mail.get('category') !== 'sent' && ( + <Fragment> +From + {mail.get('name')} + {' '} +to me + </Fragment> + )} + </Typography> + <div className={classes.opt}> + <Tooltip id="tooltip-mark" title="Stared"> + <IconButton onClick={() => toggleStar(mail)}>{mail.get('stared') ? (<Star className={classes.iconOrange} />) : (<StarBorder />) }</IconButton> + </Tooltip> + <Tooltip id="tooltip-mark" title="Mark message to"> + <IconButton + className={classes.button} + aria-label="mark" + aria-owns={anchorElOpt ? 'long-menu' : null} + aria-haspopup="true" + onClick={(event) => this.handleClickOpt(event, mail)} + > + <Bookmark /> + </IconButton> + </Tooltip> + <Tooltip id="tooltip-mark" title="Remove mail"> + <IconButton className={classes.button} aria-label="Delete" onClick={() => remove(mail)}><Delete /></IconButton> + </Tooltip> + </div> + </div> + <div className={classes.emailContent}> + <Typography variant="h6" gutterBottom>{mail.get('subject')}</Typography> + <article dangerouslySetInnerHTML={renderHTML} /> + </div> + <div className={classes.preview}> + {attachmentPreview(mail.get('attachment'))} + </div> + </section> + </ExpansionPanelDetails> + <Divider /> + <ExpansionPanelActions> + <div className={classes.action}> + <Button size="small">Forwad</Button> + <Button size="small" color="secondary" onClick={() => reply(mail)}>Reply</Button> + </div> + </ExpansionPanelActions> + </ExpansionPanel> + ); + }); + const showEmail = category => { + switch (category) { + case 'inbox': + return getEmail(inbox); + case 'stared': + return getEmail(stared); + case 'sent': + return getEmail(sent); + case 'spam': + return getEmail(spam); + case 'updates': + return getEmail(updates); + case 'social': + return getEmail(social); + case 'promos': + return getEmail(promos); + case 'forums': + return getEmail(forums); + default: + return getEmail(inbox); + } + }; + return ( + <main className={classes.content}> + <div className={classes.toolbar} /> + <Menu + id="long-menu" + anchorEl={anchorElOpt} + open={Boolean(anchorElOpt)} + onClose={this.handleCloseOpt} + className={classes.markMenu} + PaperProps={{ style: { maxHeight: ITEM_HEIGHT * 4.5, width: 200 } }} + > + <List + component="nav" + subheader={<ListSubheader component="div">Mark to... </ListSubheader>} + /> + <MenuItem selected onClick={() => this.handleMoveTo(itemToMove, 'updates')}> + <Flag className={classes.iconOrange} /> + {' '} +Updates + </MenuItem> + <MenuItem onClick={() => this.handleMoveTo(itemToMove, 'social')}> + <People className={classes.iconRed} /> + {' '} +Social + </MenuItem> + <MenuItem onClick={() => this.handleMoveTo(itemToMove, 'promos')}> + <LabelIcon className={classes.iconBlue} /> + {' '} +Promos + </MenuItem> + <MenuItem onClick={() => this.handleMoveTo(itemToMove, 'forums')}> + <QuestionAnswer className={classes.iconCyan} /> + {' '} +Forums + </MenuItem> + <Divider /> + <MenuItem onClick={() => this.handleMoveTo(itemToMove, 'spam')}> + <ReportIcon /> + {' '} +Spam + </MenuItem> + </Menu> + {showEmail(filterPage)} + </main> + ); + } +} + +EmailList.propTypes = { + classes: PropTypes.object.isRequired, + emailData: PropTypes.object.isRequired, + openMail: PropTypes.func.isRequired, + moveTo: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + toggleStar: PropTypes.func.isRequired, + reply: PropTypes.func.isRequired, + filterPage: PropTypes.string.isRequired, + keyword: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(EmailList); diff --git a/front/odiparpack/app/components/Email/EmailSidebar.js b/front/odiparpack/app/components/Email/EmailSidebar.js new file mode 100644 index 0000000..1951de2 --- /dev/null +++ b/front/odiparpack/app/components/Email/EmailSidebar.js @@ -0,0 +1,162 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import InboxIcon from '@material-ui/icons/MoveToInbox'; +import SendIcon from '@material-ui/icons/Send'; +import ReportIcon from '@material-ui/icons/Report'; +import StarIcon from '@material-ui/icons/Star'; +import Flag from '@material-ui/icons/Flag'; +import People from '@material-ui/icons/People'; +import QuestionAnswer from '@material-ui/icons/QuestionAnswer'; +import LabelIcon from '@material-ui/icons/Label'; +import Add from '@material-ui/icons/Add'; +import { + Drawer, + List, + ListItem, + ListItemIcon, + ListItemText, + Hidden, + Button, + Divider, +} from '@material-ui/core'; +import styles from './email-jss'; + + +const MenuList = props => { + const { + classes, + compose, + goto, + selected, + } = props; + return ( + <Fragment> + <List> + <ListItem> + <Button variant="contained" onClick={compose} fullWidth color="primary"> + <Add /> + {' '} +Compose + </Button> + </ListItem> + <ListItem button className={selected === 'inbox' ? classes.selected : ''} onClick={() => goto('inbox')}> + <ListItemIcon> + <InboxIcon /> + </ListItemIcon> + <ListItemText primary="Inbox" /> + </ListItem> + <ListItem button className={selected === 'stared' ? classes.selected : ''} onClick={() => goto('stared')}> + <ListItemIcon> + <StarIcon /> + </ListItemIcon> + <ListItemText primary="Stared" /> + </ListItem> + <ListItem button className={selected === 'sent' ? classes.selected : ''} onClick={() => goto('sent')}> + <ListItemIcon> + <SendIcon /> + </ListItemIcon> + <ListItemText primary="Sent" /> + </ListItem> + <ListItem button className={selected === 'spam' ? classes.selected : ''} onClick={() => goto('spam')}> + <ListItemIcon> + <ReportIcon /> + </ListItemIcon> + <ListItemText primary="Spam" /> + </ListItem> + </List> + <Divider className={classes.divider} /> + <List> + <ListItem button className={selected === 'updates' ? classes.selected : ''} onClick={() => goto('updates')}> + <ListItemIcon> + <Flag className={classes.iconOrange} /> + </ListItemIcon> + <ListItemText primary="Updates" /> + </ListItem> + <ListItem button className={selected === 'social' ? classes.selected : ''} onClick={() => goto('social')}> + <ListItemIcon> + <People className={classes.iconRed} /> + </ListItemIcon> + <ListItemText primary="Social" /> + </ListItem> + <ListItem button className={selected === 'promos' ? classes.selected : ''} onClick={() => goto('promos')}> + <ListItemIcon> + <LabelIcon className={classes.iconBlue} /> + </ListItemIcon> + <ListItemText primary="Promos" /> + </ListItem> + <ListItem button className={selected === 'forums' ? classes.selected : ''} onClick={() => goto('forums')}> + <ListItemIcon> + <QuestionAnswer className={classes.iconCyan} /> + </ListItemIcon> + <ListItemText primary="Forums" /> + </ListItem> + </List> + </Fragment> + ); +}; + +MenuList.propTypes = { + classes: PropTypes.object.isRequired, + compose: PropTypes.func.isRequired, + goto: PropTypes.func.isRequired, + selected: PropTypes.string.isRequired, +}; + +const MenuEmail = withStyles(styles)(MenuList); + +class EmailSidebar extends React.Component { + render() { + const { + classes, + compose, + goto, + selected, + handleDrawerToggle, + mobileOpen + } = this.props; + return ( + <Fragment> + <Hidden mdUp> + <Drawer + variant="temporary" + open={mobileOpen} + onClose={handleDrawerToggle} + classes={{ + paper: classes.drawerPaper, + }} + ModalProps={{ + keepMounted: true, // Better open performance on mobile. + }} + > + <div className={classes.toolbar} /> + <MenuEmail compose={compose} goto={goto} selected={selected} /> + </Drawer> + </Hidden> + <Hidden smDown> + <Drawer + variant="permanent" + className={classes.sidebar} + classes={{ + paper: classes.drawerPaper, + }} + > + <div className={classes.toolbar} /> + <MenuEmail compose={compose} goto={goto} selected={selected} /> + </Drawer> + </Hidden> + </Fragment> + ); + } +} + +EmailSidebar.propTypes = { + classes: PropTypes.object.isRequired, + compose: PropTypes.func.isRequired, + goto: PropTypes.func.isRequired, + handleDrawerToggle: PropTypes.func.isRequired, + selected: PropTypes.string.isRequired, + mobileOpen: PropTypes.bool.isRequired, +}; + +export default withStyles(styles)(EmailSidebar); diff --git a/front/odiparpack/app/components/Email/email-jss.js b/front/odiparpack/app/components/Email/email-jss.js new file mode 100644 index 0000000..6775966 --- /dev/null +++ b/front/odiparpack/app/components/Email/email-jss.js @@ -0,0 +1,238 @@ +import { fade } from '@material-ui/core/styles/colorManipulator'; +import { red, orange, indigo as blue, cyan } from '@material-ui/core/colors'; +const drawerWidth = 240; +const styles = theme => ({ + iconRed: { + color: red[500] + }, + iconOrange: { + color: orange[500] + }, + iconBlue: { + color: blue[500] + }, + iconCyan: { + color: cyan[500] + }, + appBar: { + zIndex: 130, + background: theme.palette.secondary.main, + '& ::-webkit-input-placeholder': { + color: theme.palette.common.white + }, + '& ::-moz-placeholder': { + color: theme.palette.common.white + }, + '& :-ms-input-placeholder': { + color: theme.palette.common.white + }, + '& :-moz-placeholder': { + color: theme.palette.common.white + } + }, + flex: { + flex: 1, + }, + wrapper: { + fontFamily: theme.typography.fontFamily, + position: 'relative', + marginLeft: theme.spacing(1), + borderRadius: 2, + background: fade(theme.palette.common.white, 0.15), + '&:hover': { + background: fade(theme.palette.common.white, 0.25), + }, + '& $input': { + transition: theme.transitions.create('width'), + }, + }, + addBtn: { + position: 'fixed', + bottom: 30, + right: 30, + zIndex: 1000 + }, + sidebar: { + zIndex: 120 + }, + search: { + width: theme.spacing(9), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + input: { + font: 'inherit', + padding: `${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(9)}px`, + border: 0, + display: 'block', + verticalAlign: 'middle', + whiteSpace: 'normal', + background: 'none', + margin: 0, // Reset for Safari + color: 'inherit', + width: '100%', + '&:focus': { + outline: 0, + }, + }, + drawerPaper: { + [theme.breakpoints.up('md')]: { + position: 'relative', + }, + width: drawerWidth, + background: theme.palette.grey[50], + border: 'none', + padding: 10 + }, + selected: { + background: theme.palette.secondary.light, + borderLeft: `2px solid ${theme.palette.secondary.main}`, + paddingLeft: 22, + '& h3': { + color: theme.palette.secondary.dark + } + }, + content: { + flexGrow: 1, + backgroundColor: theme.palette.background.default, + zIndex: 120, + marginBottom: theme.spacing(8), + [theme.breakpoints.up('md')]: { + padding: theme.spacing(3), + marginBottom: theme.spacing(4), + paddingLeft: 0, + }, + position: 'relative', + minWidth: 0, // So the Typography noWrap works + }, + toolbar: { + minHeight: 32 + }, + title: { + width: 205 + }, + divider: { + margin: '0 20px 0 10px' + }, + /* Email List */ + column: { + flexBasis: '33.33%', + overflow: 'hidden', + paddingRight: '0 !important', + paddingTop: 5, + marginLeft: 20 + }, + secondaryHeading: { + fontSize: 14, + color: theme.palette.text.secondary, + }, + icon: { + verticalAlign: 'bottom', + height: 20, + width: 20, + }, + details: { + alignItems: 'center', + [theme.breakpoints.down('sm')]: { + padding: `${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(3)}px` + }, + '& section': { + width: '100%' + } + }, + link: { + color: theme.palette.secondary.main, + textDecoration: 'none', + '&:hover': { + textDecoration: 'underline', + }, + }, + avatar: {}, + fromHeading: { + overflow: 'hidden', + display: 'flex', + alignItems: 'center', + '& $avatar': { + width: 30, + height: 30, + marginRight: 20 + } + }, + topAction: { + display: 'flex', + background: theme.palette.grey[100], + marginBottom: 20, + alignItems: 'center', + padding: '0 20px', + borderRadius: 2, + }, + category: { + fontSize: 12, + textTransform: 'uppercase', + display: 'flex', + '& svg': { + fontSize: 16, + marginRight: 5 + } + }, + markMenu: { + '& svg': { + marginRight: 10 + } + }, + headMail: { + flex: 1 + }, + field: { + width: '100%', + marginBottom: 20, + '& svg': { + color: theme.palette.grey[400], + fontSize: 18, + } + }, + hiddenDropzone: { + display: 'none' + }, + sendIcon: { + marginLeft: 10 + }, + item: {}, + preview: { + display: 'flex', + marginBottom: 20, + '& $item': { + maxWidth: 160, + marginBottom: 5, + marginRight: 5 + } + }, + emailSummary: { + paddingLeft: 0, + '& > div': { + [theme.breakpoints.down('sm')]: { + flexDirection: 'column' + }, + } + }, + emailContent: { + padding: theme.spacing(2), + [theme.breakpoints.down('sm')]: { + padding: `${theme.spacing(2)}px ${theme.spacing(2)}px`, + }, + }, + starBtn: { + marginRight: 10 + }, + navIconHide: { + [theme.breakpoints.up('md')]: { + display: 'none', + }, + }, +}); + +export default styles; diff --git a/front/odiparpack/app/components/Error/ErrorWrap.js b/front/odiparpack/app/components/Error/ErrorWrap.js new file mode 100644 index 0000000..5b57ab3 --- /dev/null +++ b/front/odiparpack/app/components/Error/ErrorWrap.js @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { Route, Link } from 'react-router-dom'; + +import { Typography, Button } from '@material-ui/core'; + +const styles = theme => ({ + errorWrap: { + background: theme.palette.common.white, + boxShadow: theme.shadows[2], + borderRadius: '50%', + width: 500, + height: 500, + [theme.breakpoints.down('sm')]: { + width: 300, + height: 300, + }, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + position: 'relative', + margin: `${theme.spacing(3)}px auto`, + }, + title: { + color: theme.palette.primary.main, + textShadow: `10px 6px 50px ${theme.palette.primary.main}`, + [theme.breakpoints.down('sm')]: { + fontSize: '4rem' + }, + }, + deco: { + boxShadow: theme.shadows[2], + position: 'absolute', + borderRadius: 2, + }, + button: { + marginTop: 50 + } +}); + +const ErrorWrap = (props) => ( + <Route + render={({ staticContext }) => { + if (staticContext) { + staticContext.status = 404; // eslint-disable-line + } + const { classes, title, desc } = props; + return ( + <div className={classes.errorWrap}> + <Typography className={classes.title} variant="h1">{title}</Typography> + <Typography variant="h5">{desc}</Typography> + <Button + variant="contained" + color="primary" + className={classes.button} + component={Link} + to="/app/" + > + Go To Dashboard + </Button> + </div> + ); + }} + /> +); + +ErrorWrap.propTypes = { + classes: PropTypes.object.isRequired, + desc: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(ErrorWrap); diff --git a/front/odiparpack/app/components/Forms/LockForm.js b/front/odiparpack/app/components/Forms/LockForm.js new file mode 100644 index 0000000..c667809 --- /dev/null +++ b/front/odiparpack/app/components/Forms/LockForm.js @@ -0,0 +1,123 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { Field, reduxForm } from 'redux-form/immutable'; +import ArrowForward from '@material-ui/icons/ArrowForward'; +import Help from '@material-ui/icons/Help'; +import dummy from 'ba-api/dummyContents'; +import avatarApi from 'ba-api/avatars'; +import { + Button, + Popover, + FormControl, + IconButton, + Typography, + InputAdornment, + Paper, + Avatar, +} from '@material-ui/core'; +import { TextFieldRedux } from './ReduxFormMUI'; +import styles from './user-jss'; + + +// validation functions +const required = value => (value == null ? 'Required' : undefined); + +class LockForm extends React.Component { + state = { + anchorEl: null, + }; + + handleShowHint = event => { + this.setState({ + anchorEl: event.currentTarget, + }); + }; + + handleClose = () => { + this.setState({ + anchorEl: null, + }); + }; + + render() { + const { + classes, + handleSubmit, + pristine, + submitting + } = this.props; + const { anchorEl } = this.state; + return ( + <div className={classes.formWrap}> + <Paper className={classes.lockWrap}> + <form onSubmit={handleSubmit}> + <Avatar alt="John Doe" src={avatarApi[6]} className={classes.avatar} /> + <Typography className={classes.userName} variant="h4">{dummy.user.name}</Typography> + <div> + <FormControl className={classes.formControl}> + <Field + name="password" + component={TextFieldRedux} + type="password" + label="Your Password" + required + validate={required} + className={classes.field} + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <IconButton + aria-label="Helper Hint" + onClick={this.handleShowHint} + > + <Help /> + </IconButton> + </InputAdornment> + ) + }} + /> + </FormControl> + <Popover + open={Boolean(anchorEl)} + anchorEl={anchorEl} + onClose={this.handleClose} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + > + <Typography className={classes.hint}>Hint: Type anything to unlock!</Typography> + </Popover> + </div> + <div className={classes.btnArea}> + <Button fullWidth variant="contained" color="primary" type="submit"> + Continue + <ArrowForward className={classNames(classes.rightIcon, classes.iconSmall)} disabled={submitting || pristine} /> + </Button> + </div> + </form> + </Paper> + </div> + ); + } +} + +LockForm.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, +}; + +const LockFormReduxed = reduxForm({ + form: 'immutableELockFrm', + enableReinitialize: true, +})(LockForm); + +export default withStyles(styles)(LockFormReduxed); diff --git a/front/odiparpack/app/components/Forms/LoginForm.js b/front/odiparpack/app/components/Forms/LoginForm.js new file mode 100644 index 0000000..d652480 --- /dev/null +++ b/front/odiparpack/app/components/Forms/LoginForm.js @@ -0,0 +1,147 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { Field, reduxForm } from 'redux-form/immutable'; +import { connect } from 'react-redux'; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import AllInclusive from '@material-ui/icons/AllInclusive'; +import Brightness5 from '@material-ui/icons/Brightness5'; +import People from '@material-ui/icons/People'; +import ArrowForward from '@material-ui/icons/ArrowForward'; +import { Button, IconButton, InputAdornment, FormControl, FormControlLabel } from '@material-ui/core'; +import styles from './user-jss'; +import { TextFieldRedux, CheckboxRedux } from './ReduxFormMUI'; +import { ContentDivider } from '../Divider'; +import PapperBlock from '../PapperBlock/PapperBlock'; + + +// validation functions +const required = value => (value == null ? 'Required' : undefined); +const email = value => ( + value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) + ? 'Invalid email' + : undefined +); + +class LoginForm extends React.Component { + state = { + showPassword: false + } + + handleClickShowPassword = () => { + this.setState({ showPassword: !this.state.showPassword }); + }; + + handleMouseDownPassword = event => { + event.preventDefault(); + }; + + render() { + const { + classes, + handleSubmit, + pristine, + submitting + } = this.props; + return ( + <div className={classes.formWrap}> + <PapperBlock whiteBg title="Login" desc=""> + <form onSubmit={handleSubmit}> + <div> + <FormControl className={classes.formControl}> + <Field + name="email" + component={TextFieldRedux} + placeholder="Your Email" + label="Your Email" + required + validate={[required, email]} + className={classes.field} + /> + </FormControl> + </div> + <div> + <FormControl className={classes.formControl}> + <Field + name="password" + component={TextFieldRedux} + type={this.state.showPassword ? 'text' : 'password'} + label="Your Password" + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <IconButton + aria-label="Toggle password visibility" + onClick={this.handleClickShowPassword} + onMouseDown={this.handleMouseDownPassword} + > + {this.state.showPassword ? <VisibilityOff /> : <Visibility />} + </IconButton> + </InputAdornment> + ) + }} + required + validate={required} + className={classes.field} + /> + </FormControl> + </div> + <div className={classes.btnArea}> + <FormControlLabel control={<Field name="checkbox" component={CheckboxRedux} />} label="Remember" /> + <Button variant="contained" color="primary" type="submit"> + Continue + <ArrowForward className={classNames(classes.rightIcon, classes.iconSmall)} disabled={submitting || pristine} /> + </Button> + </div> + <ContentDivider content="OR" /> + <div className={classes.btnArea}> + <Button variant="contained" size="small" className={classes.redBtn} type="button"> + <AllInclusive className={classNames(classes.leftIcon, classes.iconSmall)} /> + Socmed 1 + </Button> + <Button variant="contained" size="small" className={classes.blueBtn} type="button"> + <Brightness5 className={classNames(classes.leftIcon, classes.iconSmall)} /> + Socmed 2 + </Button> + <Button variant="contained" size="small" className={classes.cyanBtn} type="button"> + <People className={classNames(classes.leftIcon, classes.iconSmall)} /> + Socmed 3 + </Button> + </div> + <div className={classes.footer}> + Cannot Login? + <Button size="small" color="secondary" className={classes.button}>Forgot Password</Button> + | + {' '} + <Button size="small" color="secondary" className={classes.button}>Register</Button> + </div> + </form> + </PapperBlock> + </div> + ); + } +} + +LoginForm.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, +}; + +const LoginFormReduxed = reduxForm({ + form: 'immutableExample', + enableReinitialize: true, +})(LoginForm); + +const reducer = 'login'; +const FormInit = connect( + state => ({ + force: state, + initialValues: state.getIn([reducer, 'usersLogin']) + }), +)(LoginFormReduxed); + +export default withStyles(styles)(FormInit); diff --git a/front/odiparpack/app/components/Forms/MaterialDropZone.js b/front/odiparpack/app/components/Forms/MaterialDropZone.js new file mode 100644 index 0000000..c62b3fd --- /dev/null +++ b/front/odiparpack/app/components/Forms/MaterialDropZone.js @@ -0,0 +1,202 @@ +import React from 'react'; +import Dropzone from 'react-dropzone'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import FileIcon from '@material-ui/icons/Description'; +import ActionDelete from '@material-ui/icons/Delete'; +import IconButton from '@material-ui/core/IconButton'; +import Snackbar from '@material-ui/core/Snackbar'; +import CloudUpload from '@material-ui/icons/CloudUpload'; +import { lighten } from '@material-ui/core/styles/colorManipulator'; +import 'ba-styles/vendors/react-dropzone/react-dropzone.css'; +import isImage from './helpers/helpers.js'; + +const styles = theme => ({ + dropItem: { + borderColor: theme.palette.secondary.main, + background: lighten(theme.palette.secondary.light, 0.9), + borderRadius: 2 + }, + uploadIconSize: { + width: 51, + height: 51, + color: theme.palette.secondary.main, + margin: '0 auto' + }, + rightIcon: { + marginLeft: theme.spacing(1), + }, + button: { + marginTop: 20 + } +}); + +class MaterialDropZone extends React.Component { + constructor(props) { + super(props); + + this.state = { + openSnackBar: false, + errorMessage: '', + files: this.props.files, // eslint-disable-line + acceptedFiles: this.props.acceptedFiles // eslint-disable-line + }; + this.onDrop = this.onDrop.bind(this); + } + + onDrop(filesVal) { + const { files } = this.state; + const { filesLimit } = this.props; + let oldFiles = files; + const filesLimitVal = filesLimit || '3'; + oldFiles = oldFiles.concat(filesVal); + if (oldFiles.length > filesLimit) { + this.setState({ + openSnackBar: true, + errorMessage: 'Cannot upload more than ' + filesLimitVal + ' items.', + }); + } else { + this.setState({ files: oldFiles }); + } + } + + onDropRejected() { + this.setState({ + openSnackBar: true, + errorMessage: 'File too big, max size is 3MB', + }); + } + + handleRequestCloseSnackBar = () => { + this.setState({ + openSnackBar: false, + }); + }; + + handleRemove(file, fileIndex) { + const thisFiles = this.state.files; // eslint-disable-line + // This is to prevent memory leaks. + window.URL.revokeObjectURL(file.preview); + + thisFiles.splice(fileIndex, 1); + this.setState({ files: thisFiles }); + } + + render() { + const { + classes, + showPreviews, + maxSize, + text, + showButton, + filesLimit, + ...rest + } = this.props; + + const { + acceptedFiles, + files, + openSnackBar, + errorMessage + } = this.state; + const fileSizeLimit = maxSize || 3000000; + const deleteBtn = (file, index) => ( + <div className="middle"> + <IconButton onClick={() => this.handleRemove(file, index)}> + <ActionDelete className="removeBtn" /> + </IconButton> + </div> + ); + const previews = filesArray => filesArray.map((file, index) => { + const base64Img = URL.createObjectURL(file); + if (isImage(file)) { + return ( + <div key={index.toString()}> + <div className="imageContainer col fileIconImg"> + <figure className="imgWrap"><img className="smallPreviewImg" src={base64Img} alt="preview" /></figure> + {deleteBtn(file, index)} + </div> + </div> + ); + } + return ( + <div key={index.toString()}> + <div className="imageContainer col fileIconImg"> + <FileIcon className="smallPreviewImg" alt="preview" /> + {deleteBtn(file, index)} + </div> + </div> + ); + }); + let dropzoneRef; + return ( + <div> + <Dropzone + accept={acceptedFiles.join(',')} + onDrop={this.onDrop} + onDropRejected={this.onDropRejected} + acceptClassName="stripes" + rejectClassName="rejectStripes" + maxSize={fileSizeLimit} + ref={(node) => { dropzoneRef = node; }} + {...rest} + > + {({ getRootProps, getInputProps }) => ( + <div {...getRootProps()} className={classNames(classes.dropItem, 'dropZone')}> + <div className="dropzoneTextStyle"> + <input {...getInputProps()} /> + <p className="dropzoneParagraph">{text}</p> + <div className={classes.uploadIconSize}> + <CloudUpload className={classes.uploadIconSize} /> + </div> + </div> + </div> + )} + {/* end */} + </Dropzone> + {showButton && ( + <Button + className={classes.button} + fullWidth + variant="contained" + onClick={() => { + dropzoneRef.open(); + }} + color="secondary" + > + Click to upload file(s) + </Button> + )} + <div className="row preview"> + {showPreviews && previews(files)} + </div> + <Snackbar + open={openSnackBar} + message={errorMessage} + autoHideDuration={4000} + onClose={this.handleRequestCloseSnackBar} + /> + </div> + ); + } +} + +MaterialDropZone.propTypes = { + files: PropTypes.array.isRequired, + text: PropTypes.string.isRequired, + acceptedFiles: PropTypes.array, + showPreviews: PropTypes.bool.isRequired, + showButton: PropTypes.bool, + maxSize: PropTypes.number.isRequired, + filesLimit: PropTypes.number.isRequired, + classes: PropTypes.object.isRequired, +}; + +MaterialDropZone.defaultProps = { + acceptedFiles: [], + showButton: false, +}; + +export default withStyles(styles)(MaterialDropZone); diff --git a/front/odiparpack/app/components/Forms/ReduxFormMUI.js b/front/odiparpack/app/components/Forms/ReduxFormMUI.js new file mode 100644 index 0000000..383a717 --- /dev/null +++ b/front/odiparpack/app/components/Forms/ReduxFormMUI.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import TextField from '@material-ui/core/TextField'; +import Select from '@material-ui/core/Select'; +import Checkbox from '@material-ui/core/Checkbox'; +import Switch from '@material-ui/core/Switch'; + +/* Textfield */ +export const TextFieldRedux = ({ meta: { touched, error }, input, ...rest }) => ( + <TextField + {...rest} + {...input} + error={touched && Boolean(error)} + /> +); + +TextFieldRedux.propTypes = { + input: PropTypes.object.isRequired, + meta: PropTypes.object, +}; + +TextFieldRedux.defaultProps = { + meta: null, +}; +/* End */ + +/* Select */ +export const SelectRedux = ({ input, children, ...rest }) => ( + <Select + {...input} + {...rest} + > + {children} + </Select> +); + +SelectRedux.propTypes = { + input: PropTypes.object.isRequired, + children: PropTypes.node.isRequired, +}; +/* End */ + +/* Checkbox */ +export const CheckboxRedux = ({ input, ...rest }) => ( + <Checkbox + checked={input.value === '' ? false : input.value} + {...input} + {...rest} + /> +); + +CheckboxRedux.propTypes = { + input: PropTypes.object.isRequired, +}; +/* End */ + +/* Switch */ +export const SwitchRedux = ({ input, ...rest }) => ( + <Switch + checked={input.value === '' ? false : input.value} + {...input} + {...rest} + /> +); + +SwitchRedux.propTypes = { + input: PropTypes.object.isRequired, +}; +/* End */ diff --git a/front/odiparpack/app/components/Forms/RegisterForm.js b/front/odiparpack/app/components/Forms/RegisterForm.js new file mode 100644 index 0000000..2ac4c65 --- /dev/null +++ b/front/odiparpack/app/components/Forms/RegisterForm.js @@ -0,0 +1,174 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { Field, reduxForm } from 'redux-form/immutable'; +import ArrowForward from '@material-ui/icons/ArrowForward'; +import AllInclusive from '@material-ui/icons/AllInclusive'; +import Brightness5 from '@material-ui/icons/Brightness5'; +import People from '@material-ui/icons/People'; +import { Button, FormControl, FormControlLabel, Tabs, Tab } from '@material-ui/core'; +import styles from './user-jss'; +import { TextFieldRedux, CheckboxRedux } from './ReduxFormMUI'; +import PapperBlock from '../PapperBlock/PapperBlock'; + + +// validation functions +const required = value => (value == null ? 'Required' : undefined); +const email = value => ( + value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) + ? 'Invalid email' + : undefined +); + +const passwordsMatch = (value, allValues) => { + console.log(value, allValues.get('password')); + if (value !== allValues.get('password')) { + return 'Passwords dont match'; + } + return undefined; +}; + +class RegisterForm extends React.Component { + state = { + tab: 0, + }; + + handleClickShowPassword = () => { + this.setState({ showPassword: !this.state.showPassword }); + }; + + handleMouseDownPassword = event => { + event.preventDefault(); + }; + + handleChangeTab = (event, value) => { + this.setState({ tab: value }); + }; + + render() { + const { + classes, + handleSubmit, + pristine, + submitting + } = this.props; + const { tab } = this.state; + return ( + <div className={classes.formWrap}> + <PapperBlock whiteBg title="Create New Account" desc=""> + <Tabs + value={this.state.tab} + onChange={this.handleChangeTab} + indicatorColor="primary" + textColor="primary" + centered + className={classes.tab} + > + <Tab label="With Email" /> + <Tab label="With Social Network" /> + </Tabs> + {tab === 0 + && ( + <form onSubmit={handleSubmit}> + <div> + <FormControl className={classes.formControl}> + <Field + name="name" + component={TextFieldRedux} + placeholder="Username" + label="Username" + required + className={classes.field} + /> + </FormControl> + </div> + <div> + <FormControl className={classes.formControl}> + <Field + name="email" + component={TextFieldRedux} + placeholder="Your Email" + label="Your Email" + required + validate={[required, email]} + className={classes.field} + /> + </FormControl> + </div> + <div> + <FormControl className={classes.formControl}> + <Field + name="password" + component={TextFieldRedux} + type="password" + label="Your Password" + required + validate={[required, passwordsMatch]} + className={classes.field} + /> + </FormControl> + </div> + <div> + <FormControl className={classes.formControl}> + <Field + name="passwordConfirm" + component={TextFieldRedux} + type="password" + label="Re-type Password" + required + validate={[required, passwordsMatch]} + className={classes.field} + /> + </FormControl> + </div> + <div className={classNames(classes.btnArea, classes.noMargin)}> + <div className={classes.optArea}> + <FormControlLabel control={<Field name="checkbox" component={CheckboxRedux} className={classes.agree} />} label="Agree with" /> + <a href="#" className={classes.link}>Terms & Condition</a> + </div> + <Button variant="contained" color="primary" type="submit"> + Continue + <ArrowForward className={classNames(classes.rightIcon, classes.iconSmall)} disabled={submitting || pristine} /> + </Button> + </div> + </form> + ) + } + {tab === 1 + && ( + <div> + <Button fullWidth variant="contained" size="large" className={classNames(classes.redBtn, classes.socMedFull)} type="button"> + <AllInclusive className={classNames(classes.leftIcon, classes.iconSmall)} /> + Socmed 1 + </Button> + <Button fullWidth variant="contained" size="large" className={classNames(classes.blueBtn, classes.socMedFull)} type="button"> + <Brightness5 className={classNames(classes.leftIcon, classes.iconSmall)} /> + Socmed 2 + </Button> + <Button fullWidth variant="contained" size="large" className={classes.cyanBtn} type="button"> + <People className={classNames(classes.leftIcon, classes.iconSmall)} /> + Socmed 3 + </Button> + </div> + ) + } + </PapperBlock> + </div> + ); + } +} + +RegisterForm.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, +}; + +const RegisterFormReduxed = reduxForm({ + form: 'immutableExample', + enableReinitialize: true, +})(RegisterForm); + +export default withStyles(styles)(RegisterFormReduxed); diff --git a/front/odiparpack/app/components/Forms/ResetForm.js b/front/odiparpack/app/components/Forms/ResetForm.js new file mode 100644 index 0000000..95bf93b --- /dev/null +++ b/front/odiparpack/app/components/Forms/ResetForm.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { Field, reduxForm } from 'redux-form/immutable'; +import ArrowForward from '@material-ui/icons/ArrowForward'; +import { Button, FormControl } from '@material-ui/core'; +import styles from './user-jss'; +import PapperBlock from '../PapperBlock/PapperBlock'; +import { TextFieldRedux } from './ReduxFormMUI'; + + +// validation functions +const required = value => (value == null ? 'Required' : undefined); +const email = value => ( + value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value) + ? 'Invalid email' + : undefined +); + +class ResetForm extends React.Component { + render() { + const { + classes, + handleSubmit, + pristine, + submitting + } = this.props; + return ( + <div className={classes.formWrap}> + <PapperBlock whiteBg title="Reset Password" desc="We will send reset password link to Your Email"> + <form onSubmit={handleSubmit}> + <div> + <FormControl className={classes.formControl}> + <Field + name="email" + component={TextFieldRedux} + placeholder="Your Email" + label="Your Email" + required + validate={[required, email]} + className={classes.field} + /> + </FormControl> + </div> + <div className={classes.btnArea}> + <Button variant="contained" color="primary" type="submit"> + Send ResetLink + <ArrowForward className={classNames(classes.rightIcon, classes.iconSmall)} disabled={submitting || pristine} /> + </Button> + </div> + </form> + </PapperBlock> + </div> + ); + } +} + +ResetForm.propTypes = { + classes: PropTypes.object.isRequired, + handleSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, +}; + +const ResetFormReduxed = reduxForm({ + form: 'immutableEResetFrm', + enableReinitialize: true, +})(ResetForm); + +export default withStyles(styles)(ResetFormReduxed); diff --git a/front/odiparpack/app/components/Forms/helpers/helpers.js b/front/odiparpack/app/components/Forms/helpers/helpers.js new file mode 100644 index 0000000..99c953a --- /dev/null +++ b/front/odiparpack/app/components/Forms/helpers/helpers.js @@ -0,0 +1,8 @@ +export default function isImage(file) { + const fileName = file.name || file.path; + const suffix = fileName.substr(fileName.indexOf('.') + 1).toLowerCase(); + if (suffix === 'jpg' || suffix === 'jpeg' || suffix === 'bmp' || suffix === 'png') { + return true; + } + return false; +} diff --git a/front/odiparpack/app/components/Forms/user-jss.js b/front/odiparpack/app/components/Forms/user-jss.js new file mode 100644 index 0000000..5b9ae4a --- /dev/null +++ b/front/odiparpack/app/components/Forms/user-jss.js @@ -0,0 +1,179 @@ +import { cyan, indigo, red } from '@material-ui/core/colors'; +const styles = theme => ({ + root: { + display: 'flex', + width: '100%', + zIndex: 1, + position: 'relative' + }, + container: { + overflow: 'hidden', + display: 'flex', + alignItems: 'center', + width: '100%', + [theme.breakpoints.down('md')]: { + overflow: 'hidden' + }, + }, + formControl: { + width: '100%', + marginBottom: theme.spacing(3) + }, + loginWrap: { + [theme.breakpoints.up('md')]: { + width: 860 + }, + }, + formWrap: { + [theme.breakpoints.up('md')]: { + marginTop: -24 + }, + }, + btnArea: { + justifyContent: 'space-between', + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(3), + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + '& button': { + width: '100%', + margin: 5 + } + }, + }, + noMargin: { + margin: 0 + }, + optArea: { + justifyContent: 'space-between', + display: 'flex', + alignItems: 'center', + width: '100%', + [theme.breakpoints.up('sm')]: { + width: '60%' + }, + }, + redBtn: { + color: theme.palette.getContrastText(red[500]), + backgroundColor: red[500], + '&:hover': { + backgroundColor: red[700], + }, + }, + blueBtn: { + color: theme.palette.getContrastText(indigo[500]), + backgroundColor: indigo[500], + '&:hover': { + backgroundColor: indigo[700], + }, + }, + cyanBtn: { + color: theme.palette.getContrastText(cyan[700]), + backgroundColor: cyan[500], + '&:hover': { + backgroundColor: cyan[700], + }, + }, + leftIcon: { + marginRight: theme.spacing(1), + }, + rightIcon: { + marginLeft: theme.spacing(1), + }, + iconSmall: { + fontSize: 20, + }, + footer: { + textAlign: 'center', + padding: 5, + background: theme.palette.grey[100], + fontSize: 14, + position: 'relative' + }, + welcomeWrap: { + position: 'relative' + }, + welcome: { + background: theme.palette.secondary.light, + position: 'absolute', + width: '100%', + height: 'calc(100% + 30px)', + padding: '20px 50px', + top: -15, + left: 2, + boxShadow: theme.shadows[5], + borderRadius: 2, + display: 'flex', + alignItems: 'center', + overflow: 'hidden' + }, + brand: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + position: 'relative', + marginBottom: 20, + '& img': { + width: 32 + }, + '& h3': { + fontSize: 18, + margin: 0, + paddingLeft: 10, + fontWeight: 500, + color: theme.palette.grey[700] + } + }, + brandText: { + marginTop: 10, + color: 'rgba(0, 0, 0, 0.54)', + }, + decoBottom: { + fontSize: 480, + position: 'absolute', + left: 10, + bottom: -190, + opacity: 0.1, + color: theme.palette.secondary.dark + }, + tab: { + marginBottom: 20, + [theme.breakpoints.up('md')]: { + marginTop: theme.spacing(1) * -3, + }, + }, + link: { + fontSize: 12, + marginLeft: -30, + color: theme.palette.secondary.main, + textDecoration: 'none', + '&:hover': { + textDecoration: 'underline' + } + }, + socMedFull: { + marginBottom: theme.spacing(2) + }, + lockWrap: { + textAlign: 'center', + padding: theme.spacing(3) + }, + avatar: { + width: 150, + height: 150, + margin: '5px auto 30px', + [theme.breakpoints.up('md')]: { + margin: '-75px auto 30px', + }, + boxShadow: theme.shadows[8] + }, + userName: { + marginBottom: theme.spacing(3) + }, + hint: { + padding: theme.spacing(1) + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Gallery/PhotoGallery.js b/front/odiparpack/app/components/Gallery/PhotoGallery.js new file mode 100644 index 0000000..3877bba --- /dev/null +++ b/front/odiparpack/app/components/Gallery/PhotoGallery.js @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import 'ba-styles/vendors/image-lightbox/image-lightbox.css'; +import { Typography, ButtonBase } from '@material-ui/core'; +import ImageLightbox from '../ImageLightbox/ImageLightbox'; +import styles from './photo-jss'; + + +class PhotoGallery extends React.Component { + constructor(props) { + super(props); + + this.state = { + photoIndex: 0, + isOpen: false, + }; + } + + openPopup = (photoIndex) => { + this.setState({ isOpen: true, photoIndex }); + } + + render() { + const { photoIndex, isOpen } = this.state; + const { classes, imgData } = this.props; + return ( + <div> + {isOpen && ( + <ImageLightbox + mainSrc={imgData[photoIndex].img} + nextSrc={imgData[(photoIndex + 1) % imgData.length].img} + prevSrc={imgData[(photoIndex + (imgData.length - 1)) % imgData.length].img} + onCloseRequest={() => this.setState({ isOpen: false })} + onMovePrevRequest={() => this.setState({ + photoIndex: (photoIndex + (imgData.length - 1)) % imgData.length, + }) + } + onMoveNextRequest={() => this.setState({ + photoIndex: (photoIndex + 1) % imgData.length, + }) + } + /> + )} + <div className={classes.masonry}> + { + imgData.map((thumb, index) => ( + <figure className={classes.item} key={index.toString()}> + <ButtonBase + focusRipple + className={classes.image} + focusVisibleClassName={classes.focusVisible} + onClick={() => this.openPopup(index)} + > + <img src={thumb.img} alt={thumb.title} /> + <span className={classes.imageBackdrop} /> + <span className={classes.imageButton}> + <Typography + component="span" + variant="subtitle1" + color="inherit" + className={classes.imageTitle} + > + {thumb.title} + <span className={classes.imageMarked} /> + </Typography> + </span> + </ButtonBase> + </figure> + )) + } + </div> + </div> + ); + } +} + +PhotoGallery.propTypes = { + classes: PropTypes.object.isRequired, + imgData: PropTypes.array.isRequired +}; + +export default withStyles(styles)(PhotoGallery); diff --git a/front/odiparpack/app/components/Gallery/ProductDetail.js b/front/odiparpack/app/components/Gallery/ProductDetail.js new file mode 100644 index 0000000..f05852f --- /dev/null +++ b/front/odiparpack/app/components/Gallery/ProductDetail.js @@ -0,0 +1,195 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Slider from 'react-slick'; +import CloseIcon from '@material-ui/icons/Close'; +import AddShoppingCart from '@material-ui/icons/AddShoppingCart'; +import imgData from 'ba-api/imgData'; +import Type from 'ba-styles/Typography.scss'; +import 'ba-styles/vendors/slick-carousel/slick-carousel.css'; +import 'ba-styles/vendors/slick-carousel/slick.css'; +import 'ba-styles/vendors/slick-carousel/slick-theme.css'; +import { + Typography, + Grid, + Dialog, + AppBar, + Toolbar, + IconButton, + Slide, + Button, + Chip, + TextField, +} from '@material-ui/core'; +import Rating from '../Rating/Rating'; +import styles from './product-jss'; + +const getThumb = imgData.map(a => a.thumb); + +const Transition = React.forwardRef(function Transition(props, ref) { // eslint-disable-line + return <Slide direction="up" ref={ref} {...props} />; +}); + +class ProductDetail extends React.Component { + state = { + qty: 1, + } + + handleQtyChange = event => { + this.setState({ qty: event.target.value }); + } + + submitToCart = itemAttr => { + this.props.handleAddToCart(itemAttr); + this.props.close(); + } + + render() { + const { + classes, + open, + close, + detailContent, + productIndex + } = this.props; + + const { qty } = this.state; + + const itemAttr = (item) => { + if (item !== undefined) { + return { + id: detailContent.getIn([productIndex, 'id']), + name: detailContent.getIn([productIndex, 'name']), + thumbnail: detailContent.getIn([productIndex, 'thumbnail']), + price: detailContent.getIn([productIndex, 'price']), + quantity: qty + }; + } + return false; + }; + + const settings = { + customPaging: (i) => ( + <a> + <img src={getThumb[i]} alt="thumb" /> + </a> + ), + infinite: true, + dots: true, + slidesToShow: 1, + slidesToScroll: 1, + }; + + return ( + <Dialog + fullScreen + open={open} + onClose={close} + TransitionComponent={Transition} + > + <AppBar className={classes.appBar}> + <Toolbar> + <Typography variant="h6" color="inherit" className={classes.flex}> + {detailContent.getIn([productIndex, 'name'])} + </Typography> + <IconButton color="inherit" onClick={() => close()} aria-label="Close"> + <CloseIcon /> + </IconButton> + </Toolbar> + </AppBar> + <div className={classes.detailContainer}> + <Grid container className={classes.root} spacing={3}> + <Grid item md={5} sm={12} xs={12}> + <div className="container thumb-nav"> + <Slider {...settings}> + {imgData.map((item, index) => { + if (index >= 5) { + return false; + } + return ( + <div key={index.toString()} className={classes.item}> + <img src={item.img} alt={item.title} /> + </div> + ); + })} + </Slider> + </div> + </Grid> + <Grid item md={7} sm={12} xs={12}> + <section className={classes.detailWrap}> + <Typography noWrap gutterBottom variant="h5" className={classes.title} component="h2"> + {detailContent.getIn([productIndex, 'name'])} + </Typography> + <div className={classes.price}> + <Typography variant="h5"> + <span> +$ + {detailContent.getIn([productIndex, 'price'])} + </span> + </Typography> + {detailContent.getIn([productIndex, 'discount']) !== '' && ( + <Fragment> + <Typography variant="caption" component="h5"> + <span className={Type.lineThrought}> +$ + {detailContent.getIn([productIndex, 'prevPrice'])} + </span> + </Typography> + <Chip label={'Discount ' + detailContent.getIn([productIndex, 'discount'])} className={classes.chipDiscount} /> + </Fragment> + )} + {detailContent.getIn([productIndex, 'soldout']) && ( + <Chip label="Sold Out" className={classes.chipSold} /> + )} + </div> + <div className={classes.ratting}> + <Rating value={detailContent.getIn([productIndex, 'ratting'])} max={5} readOnly /> + </div> + <Typography component="p" className={classes.desc}> + {detailContent.getIn([productIndex, 'desc'])} + </Typography> + {!detailContent.getIn([productIndex, 'soldout']) && ( + <div className={classes.btnArea}> + <Typography variant="subtitle1"> + Quantity : + </Typography> + <TextField + type="number" + InputLabelProps={{ + shrink: true, + }} + margin="none" + value={qty} + className={classes.quantity} + onChange={this.handleQtyChange} + /> + <Button variant="contained" onClick={() => this.submitToCart(itemAttr(detailContent))} color="secondary"> + <AddShoppingCart /> + {' '} +Add to cart + </Button> + </div> + )} + </section> + </Grid> + </Grid> + </div> + </Dialog> + ); + } +} + +ProductDetail.propTypes = { + classes: PropTypes.object.isRequired, + open: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + handleAddToCart: PropTypes.func.isRequired, + detailContent: PropTypes.object.isRequired, + productIndex: PropTypes.number, +}; + +ProductDetail.defaultProps = { + productIndex: undefined +}; + +export default withStyles(styles)(ProductDetail); diff --git a/front/odiparpack/app/components/Gallery/ProductGallery.js b/front/odiparpack/app/components/Gallery/ProductGallery.js new file mode 100644 index 0000000..94f6c1e --- /dev/null +++ b/front/odiparpack/app/components/Gallery/ProductGallery.js @@ -0,0 +1,152 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import ViewList from '@material-ui/icons/ViewList'; +import GridOn from '@material-ui/icons/GridOn'; +import { Grid, Typography, Button } from '@material-ui/core'; +import ProductCard from '../CardPaper/ProductCard'; +import ProductDetail from './ProductDetail'; + + +const styles = theme => ({ + result: { + margin: theme.spacing(1) + }, + option: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 10 + }, + button: { + fontSize: 12, + '& svg': { + marginRight: 10 + } + }, + appBar: { + position: 'relative', + }, + flex: { + flex: 1, + }, +}); + +class ProductGallery extends React.Component { + state = { + listView: false, + open: false, + } + + handleDetailOpen = (product) => { + this.setState({ open: true }); + this.props.showDetail(product); + }; + + handleClose = () => { + this.setState({ open: false }); + }; + + handleSwitchView = () => { + this.setState({ + listView: !this.state.listView + }); + } + + render() { + const { classes } = this.props; + const { listView, open } = this.state; + const { + dataProduct, + handleAddToCart, + productIndex, + keyword, + } = this.props; + + const getTotalResult = dataArray => { + let totalResult = 0; + for (let i = 0; i < dataArray.size; i += 1) { + if (dataArray.getIn([i, 'name']) === undefined) { + return false; + } + if (dataArray.getIn([i, 'name']).toLowerCase().indexOf(keyword) !== -1) { + totalResult += 1; + } + } + return totalResult; + }; + + return ( + <div> + <ProductDetail + open={open} + close={this.handleClose} + detailContent={dataProduct} + productIndex={productIndex} + handleAddToCart={handleAddToCart} + /> + <section className={classes.option}> + <Typography variant="caption" className={classes.result}> + {getTotalResult(dataProduct)} + {' '} +Results + </Typography> + <Button onClick={this.handleSwitchView} className={classes.button} size="small"> + {listView ? <GridOn /> : <ViewList />} + {listView ? 'Grid View' : 'List View'} + </Button> + </section> + <Grid + container + alignItems="flex-start" + justify="flex-start" + direction="row" + spacing={3} + > + { + dataProduct.map((product, index) => { + if (product.get('name').toLowerCase().indexOf(keyword) === -1) { + return false; + } + const itemAttr = { + id: product.get('id'), + name: product.get('name'), + thumbnail: product.get('thumbnail'), + price: product.get('price'), + quantity: 1 + }; + return ( + <Grid item md={listView ? 12 : 4} sm={listView ? 12 : 6} xs={12} key={index.toString()}> + <ProductCard + list={listView} + name={product.get('name')} + thumbnail={product.get('thumbnail')} + desc={product.get('desc')} + ratting={product.get('ratting')} + price={product.get('price')} + prevPrice={product.get('prevPrice')} + discount={product.get('discount')} + soldout={product.get('soldout')} + detailOpen={() => this.handleDetailOpen(product)} + addToCart={() => handleAddToCart(itemAttr)} + /> + </Grid> + ); + }) + } + </Grid> + </div> + ); + } +} + +ProductGallery.propTypes = { + classes: PropTypes.object.isRequired, + dataProduct: PropTypes.object.isRequired, + handleAddToCart: PropTypes.func.isRequired, + showDetail: PropTypes.func.isRequired, + productIndex: PropTypes.number.isRequired, + keyword: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(ProductGallery); diff --git a/front/odiparpack/app/components/Gallery/photo-jss.js b/front/odiparpack/app/components/Gallery/photo-jss.js new file mode 100644 index 0000000..61a6961 --- /dev/null +++ b/front/odiparpack/app/components/Gallery/photo-jss.js @@ -0,0 +1,79 @@ +const styles = theme => ({ + masonry: { /* Masonry container */ + [theme.breakpoints.up('sm')]: { + columnCount: 2, + }, + [theme.breakpoints.up('md')]: { + columnCount: 3, + }, + columnGap: '1em', + columnFill: 'initial', + marginTop: 20 + }, + item: { + display: 'inline-table', + margin: `0 0 ${theme.spacing(2)}px`, + width: '100%', + boxShadow: theme.shadows[4], + overflow: 'hidden', + borderRadius: 2, + transition: 'box-shadow .3s', + '&:hover': { + cursor: 'pointer', + boxShadow: theme.shadows[7], + }, + '& img': { + marginBottom: -7 + } + }, + image: { + position: 'relative', + [theme.breakpoints.down('xs')]: { + width: '100% !important', // Overrides inline-style + }, + '&:hover, &$focusVisible': { + zIndex: 1, + '& $imageBackdrop': { + opacity: 0.15, + }, + }, + }, + focusVisible: {}, + imageButton: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + display: 'flex', + alignItems: 'flex-end', + justifyContent: 'center', + color: theme.palette.common.white, + paddingBottom: 10 + }, + imageBackdrop: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundColor: theme.palette.common.black, + opacity: 0, + transition: theme.transitions.create('opacity'), + }, + imageTitle: { + position: 'relative', + padding: `${theme.spacing(2)}px ${theme.spacing(4)}px ${theme.spacing(1) + 6}px`, + }, + imageMarked: { + height: 3, + width: 18, + backgroundColor: theme.palette.common.white, + position: 'absolute', + bottom: -2, + left: 'calc(50% - 9px)', + transition: theme.transitions.create('opacity'), + }, +}); + +export default styles; diff --git a/front/odiparpack/app/components/Gallery/product-jss.js b/front/odiparpack/app/components/Gallery/product-jss.js new file mode 100644 index 0000000..230ebf0 --- /dev/null +++ b/front/odiparpack/app/components/Gallery/product-jss.js @@ -0,0 +1,87 @@ +import { blueGrey as dark } from '@material-ui/core/colors'; +const styles = theme => ({ + root: { + flexGrow: 1, + }, + rootSlider: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center' + }, + item: { + textAlign: 'center', + '& img': { + margin: '10px auto' + } + }, + appBar: { + position: 'relative', + }, + flex: { + flex: 1, + }, + detailContainer: { + margin: '-16px auto 0', + maxWidth: '100%', + [theme.breakpoints.up('lg')]: { + maxWidth: 1080, + }, + [theme.breakpoints.up('md')]: { + maxWidth: 960, + paddingTop: 40, + marginTop: 0 + }, + [theme.breakpoints.down('sm')]: { + overflowX: 'hidden', + } + }, + chipDiscount: { + background: theme.palette.primary.light, + color: theme.palette.primary.dark, + marginBottom: 10, + }, + chipSold: { + background: dark[500], + color: theme.palette.getContrastText(dark[500]), + marginBottom: 10, + }, + detailWrap: { + padding: 30 + }, + title: { + marginBottom: 30 + }, + price: { + display: 'flex', + alignItems: 'center', + marginTop: 30, + padding: '8px 12px', + '& > *': { + marginRight: 10 + } + }, + ratting: { + borderBottom: `1px solid ${theme.palette.grey[400]}`, + marginBottom: 20, + }, + btnArea: { + display: 'flex', + alignItems: 'center', + marginTop: 20, + background: theme.palette.grey[100], + padding: '10px 20px' + }, + quantity: { + width: 40, + marginRight: 40, + marginLeft: 10, + '& input': { + textAlign: 'right' + } + }, + desc: { + padding: '10px 0' + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Header/Header.js b/front/odiparpack/app/components/Header/Header.js new file mode 100644 index 0000000..e1d0bf5 --- /dev/null +++ b/front/odiparpack/app/components/Header/Header.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import SearchIcon from '@material-ui/icons/Search'; +import MenuIcon from '@material-ui/icons/Menu'; +import { AppBar, Toolbar, IconButton, Hidden } from '@material-ui/core'; +import UserMenu from './UserMenu'; +import styles from './header-jss'; + +function Header(props) { + const { + classes, + toggleDrawerOpen, + margin, + turnDarker, + } = props; + + return ( + <AppBar + className={ + classNames( + classes.appBar, + margin && classes.appBarShift, + classes.appbar, + turnDarker && classes.darker + ) + } + > + <Toolbar disableGutters> + <IconButton + className={classes.menuButton} + color="inherit" + aria-label="Menu" + onClick={toggleDrawerOpen} + > + <MenuIcon /> + </IconButton> + <div className={classes.flex}> + <div className={classes.wrapper}> + <div className={classes.search}> + <SearchIcon /> + </div> + <input className={classes.input} placeholder="Search" /> + </div> + </div> + <Hidden xsDown> + <span className={classes.separatorV} /> + </Hidden> + <UserMenu /> + </Toolbar> + </AppBar> + ); +} + +Header.propTypes = { + classes: PropTypes.object.isRequired, + toggleDrawerOpen: PropTypes.func.isRequired, + margin: PropTypes.bool.isRequired, + turnDarker: PropTypes.bool.isRequired, +}; + +export default withStyles(styles)(Header); diff --git a/front/odiparpack/app/components/Header/UserMenu.js b/front/odiparpack/app/components/Header/UserMenu.js new file mode 100644 index 0000000..3ec891e --- /dev/null +++ b/front/odiparpack/app/components/Header/UserMenu.js @@ -0,0 +1,177 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { Link } from 'react-router-dom'; +import Avatar from '@material-ui/core/Avatar'; +import IconButton from '@material-ui/core/IconButton'; +import Button from '@material-ui/core/Button'; +import Info from '@material-ui/icons/Info'; +import Warning from '@material-ui/icons/Warning'; +import Check from '@material-ui/icons/CheckCircle'; +import Error from '@material-ui/icons/RemoveCircle'; +import ExitToApp from '@material-ui/icons/ExitToApp'; +import Badge from '@material-ui/core/Badge'; +import Divider from '@material-ui/core/Divider'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import Notification from '@material-ui/icons/Notifications'; +import dummy from 'ba-api/dummyContents'; +import messageStyles from 'ba-styles/Messages.scss'; +import avatarApi from 'ba-api/avatars'; +import link from 'ba-api/link'; +import styles from './header-jss'; + +function UserMenu(props) { + const { classes, dark } = props; + const [anchorEl, setAnchorEl] = useState(null); + const [openMenu, setOpenMenu] = useState(null); + + const handleMenu = menu => (event) => { + setOpenMenu(openMenu === menu ? null : menu); + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setOpenMenu(null); + setAnchorEl(null); + }; + + return ( + <div> + <IconButton + aria-haspopup="true" + onClick={handleMenu('notification')} + color="inherit" + className={classNames(classes.notifIcon, dark ? classes.dark : classes.light)} + > + <Badge className={classes.badge} badgeContent={4} color="secondary"> + <Notification /> + </Badge> + </IconButton> + <Menu + id="menu-notification" + anchorEl={anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + className={classes.notifMenu} + PaperProps={{ + style: { + width: 350, + }, + }} + open={openMenu === 'notification'} + onClose={handleClose} + > + <MenuItem onClick={handleClose}> + <div className={messageStyles.messageInfo}> + <ListItemAvatar> + <Avatar alt="User Name" src={avatarApi[0]} /> + </ListItemAvatar> + <ListItemText primary={dummy.text.subtitle} className={classes.textNotif} secondary={dummy.text.date} /> + </div> + </MenuItem> + <Divider variant="inset" /> + <MenuItem onClick={handleClose}> + <div className={messageStyles.messageInfo}> + <ListItemAvatar> + <Avatar className={messageStyles.icon}> + <Info /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dummy.text.sentences} className={classes.textNotif} secondary={dummy.text.date} /> + </div> + </MenuItem> + <Divider variant="inset" /> + <MenuItem onClick={handleClose}> + <div className={messageStyles.messageSuccess}> + <ListItemAvatar> + <Avatar className={messageStyles.icon}> + <Check /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dummy.text.subtitle} className={classes.textNotif} secondary={dummy.text.date} /> + </div> + </MenuItem> + <Divider variant="inset" /> + <MenuItem onClick={handleClose}> + <div className={messageStyles.messageWarning}> + <ListItemAvatar> + <Avatar className={messageStyles.icon}> + <Warning /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={dummy.text.subtitle} className={classes.textNotif} secondary={dummy.text.date} /> + </div> + </MenuItem> + <Divider variant="inset" /> + <MenuItem onClick={handleClose}> + <div className={messageStyles.messageError}> + <ListItemAvatar> + <Avatar className={messageStyles.icon}> + <Error /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Suspendisse pharetra pulvinar sollicitudin. Aenean ut orci eu odio cursus lobortis eget tempus velit. " className={classes.textNotif} secondary="Jan 9, 2016" /> + </div> + </MenuItem> + </Menu> + <Button onClick={handleMenu('user-setting')}> + <Avatar + alt={dummy.user.name} + src={dummy.user.avatar} + /> + </Button> + <Menu + id="menu-appbar" + anchorEl={anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + open={openMenu === 'user-setting'} + onClose={handleClose} + > + <MenuItem onClick={handleClose} component={Link} to={link.profile}>My Profile</MenuItem> + <MenuItem onClick={handleClose} component={Link} to={link.calendar}>My Calendar</MenuItem> + <MenuItem onClick={handleClose} component={Link} to={link.email}> + My Inbox + <ListItemIcon> + <Badge className={classNames(classes.badge, classes.badgeMenu)} badgeContent={2} color="secondary" /> + </ListItemIcon> + </MenuItem> + <Divider /> + <MenuItem onClick={handleClose} component={Link} to="/"> + <ListItemIcon> + <ExitToApp /> + </ListItemIcon> + Log Out + </MenuItem> + </Menu> + </div> + ); +} + +UserMenu.propTypes = { + classes: PropTypes.object.isRequired, + dark: PropTypes.bool, +}; + +UserMenu.defaultProps = { + dark: false +}; + +export default withStyles(styles)(UserMenu); diff --git a/front/odiparpack/app/components/Header/header-jss.js b/front/odiparpack/app/components/Header/header-jss.js new file mode 100644 index 0000000..e0a1d6b --- /dev/null +++ b/front/odiparpack/app/components/Header/header-jss.js @@ -0,0 +1,166 @@ +import { fade } from '@material-ui/core/styles/colorManipulator'; +const drawerWidth = 240; + +const styles = theme => ({ + appBar: { + position: 'fixed', + zIndex: theme.zIndex.drawer + 1, + transition: theme.transitions.create(['width', 'margin', 'background'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + boxShadow: 'none !important', + '& ::-webkit-input-placeholder': { /* Chrome/Opera/Safari */ + color: 'rgba(255,255,255,.3)' + }, + '& ::-moz-placeholder': { /* Firefox 19+ */ + color: 'rgba(255,255,255,.3)' + }, + '& :-ms-input-placeholder': { /* IE 10+ */ + color: 'rgba(255,255,255,.3)' + }, + '& :-moz-placeholder': { /* Firefox 18- */ + color: 'rgba(255,255,255,.3)' + }, + '& $menuButton': { + marginLeft: theme.spacing(2) + } + }, + flex: { + flex: 1, + textAlign: 'right' + }, + appBarShift: { + transition: theme.transitions.create(['width', 'margin', 'background'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + [theme.breakpoints.up('lg')]: { + marginLeft: drawerWidth, + width: `calc(100% - ${drawerWidth}px)`, + }, + '& $menuButton': { + marginLeft: 0 + } + }, + menuButton: { + [theme.breakpoints.up('lg')]: { + marginLeft: 0, + } + }, + hide: { + display: 'none', + }, + textField: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + width: 200, + }, + container: { + display: 'flex', + flexWrap: 'wrap', + }, + wrapper: { + fontFamily: theme.typography.fontFamily, + position: 'relative', + marginRight: theme.spacing(2), + marginLeft: theme.spacing(1), + borderRadius: 2, + background: fade(theme.palette.common.white, 0.15), + display: 'inline-block', + '&:hover': { + background: fade(theme.palette.common.white, 0.25), + }, + '& $input': { + transition: theme.transitions.create('width'), + width: 180, + '&:focus': { + width: 350, + }, + [theme.breakpoints.down('xs')]: { + display: 'none' + }, + }, + }, + search: { + width: theme.spacing(9), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + [theme.breakpoints.down('xs')]: { + display: 'none' + }, + }, + input: { + font: 'inherit', + padding: `${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(9)}px`, + border: 0, + display: 'block', + verticalAlign: 'middle', + whiteSpace: 'normal', + background: 'none', + margin: 0, // Reset for Safari + color: 'inherit', + width: '100%', + '&:focus': { + outline: 0, + }, + }, + userMenu: { + display: 'flex', + alignItems: 'center' + }, + popperClose: { + pointerEvents: 'none', + zIndex: 2 + }, + darker: { + background: theme.palette.primary.dark, + '&:after': { + content: '""', + left: -240, + width: 'calc(100% + 240px)', + position: 'absolute', + bottom: -2, + height: 1, + background: '#000', + filter: 'blur(3px)' + } + }, + separatorV: { + borderLeft: `1px solid ${theme.palette.grey[300]}`, + height: 20, + margin: '0 10px', + opacity: 0.4 + }, + notifMenu: { + width: 350, + '& li': { + height: 'auto', + '& h3': { + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis' + } + } + }, + badgeMenu: { + '& span': { + top: 0, + right: -30 + } + }, + textNotif: { + '& span': { + display: 'block', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis' + } + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/ImageLightbox/ImageLightbox.js b/front/odiparpack/app/components/ImageLightbox/ImageLightbox.js new file mode 100644 index 0000000..86e18a6 --- /dev/null +++ b/front/odiparpack/app/components/ImageLightbox/ImageLightbox.js @@ -0,0 +1,1794 @@ +/* eslint-disable */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'react-modal'; +import { + translate, + getWindowWidth, + getWindowHeight, + getHighestSafeWindowContext, +} from './util'; +import { + KEYS, + MIN_ZOOM_LEVEL, + MAX_ZOOM_LEVEL, + ZOOM_RATIO, + WHEEL_MOVE_X_THRESHOLD, + WHEEL_MOVE_Y_THRESHOLD, + ZOOM_BUTTON_INCREMENT_SIZE, + ACTION_NONE, + ACTION_MOVE, + ACTION_SWIPE, + ACTION_PINCH, + SOURCE_ANY, + SOURCE_MOUSE, + SOURCE_TOUCH, + SOURCE_POINTER, + MIN_SWIPE_DISTANCE, +} from './constant'; + +class ReactImageLightbox extends Component { + static isTargetMatchImage(target) { + return target && /ril-image-current/.test(target.className); + } + + static parseMouseEvent(mouseEvent) { + return { + id: 'mouse', + source: SOURCE_MOUSE, + x: parseInt(mouseEvent.clientX, 10), + y: parseInt(mouseEvent.clientY, 10), + }; + } + + static parseTouchPointer(touchPointer) { + return { + id: touchPointer.identifier, + source: SOURCE_TOUCH, + x: parseInt(touchPointer.clientX, 10), + y: parseInt(touchPointer.clientY, 10), + }; + } + + static parsePointerEvent(pointerEvent) { + return { + id: pointerEvent.pointerId, + source: SOURCE_POINTER, + x: parseInt(pointerEvent.clientX, 10), + y: parseInt(pointerEvent.clientY, 10), + }; + } + + // Request to transition to the previous image + static getTransform({ + x = 0, + y = 0, + zoom = 1, + width, + targetWidth + }) { + let nextX = x; + const windowWidth = getWindowWidth(); + if (width > windowWidth) { + nextX += (windowWidth - width) / 2; + } + const scaleFactor = zoom * (targetWidth / width); + + return { + transform: `translate3d(${nextX}px,${y}px,0) scale3d(${scaleFactor},${scaleFactor},1)`, + }; + } + + constructor(props) { + super(props); + + this.state = { + //----------------------------- + // Animation + //----------------------------- + + // Lightbox is closing + // When Lightbox is mounted, if animation is enabled it will open with the reverse of the closing animation + isClosing: !props.animationDisabled, + + // Component parts should animate (e.g., when images are moving, or image is being zoomed) + shouldAnimate: false, + + //----------------------------- + // Zoom settings + //----------------------------- + // Zoom level of image + zoomLevel: MIN_ZOOM_LEVEL, + + //----------------------------- + // Image position settings + //----------------------------- + // Horizontal offset from center + offsetX: 0, + + // Vertical offset from center + offsetY: 0, + + // image load error for srcType + loadErrorStatus: {}, + }; + + this.closeIfClickInner = this.closeIfClickInner.bind(this); + this.handleImageDoubleClick = this.handleImageDoubleClick.bind(this); + this.handleImageMouseWheel = this.handleImageMouseWheel.bind(this); + this.handleKeyInput = this.handleKeyInput.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); + this.handleMouseDown = this.handleMouseDown.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleOuterMousewheel = this.handleOuterMousewheel.bind(this); + this.handleTouchStart = this.handleTouchStart.bind(this); + this.handleTouchMove = this.handleTouchMove.bind(this); + this.handleTouchEnd = this.handleTouchEnd.bind(this); + this.handlePointerEvent = this.handlePointerEvent.bind(this); + this.handleCaptionMousewheel = this.handleCaptionMousewheel.bind(this); + this.handleWindowResize = this.handleWindowResize.bind(this); + this.handleZoomInButtonClick = this.handleZoomInButtonClick.bind(this); + this.handleZoomOutButtonClick = this.handleZoomOutButtonClick.bind(this); + this.requestClose = this.requestClose.bind(this); + this.requestMoveNext = this.requestMoveNext.bind(this); + this.requestMovePrev = this.requestMovePrev.bind(this); + } + + componentWillMount() { + // Timeouts - always clear it before umount + this.timeouts = []; + + // Current action + this.currentAction = ACTION_NONE; + + // Events source + this.eventsSource = SOURCE_ANY; + + // Empty pointers list + this.pointerList = []; + + // Prevent inner close + this.preventInnerClose = false; + this.preventInnerCloseTimeout = null; + + // Used to disable animation when changing props.mainSrc|nextSrc|prevSrc + this.keyPressed = false; + + // Used to store load state / dimensions of images + this.imageCache = {}; + + // Time the last keydown event was called (used in keyboard action rate limiting) + this.lastKeyDownTime = 0; + + // Used for debouncing window resize event + this.resizeTimeout = null; + + // Used to determine when actions are triggered by the scroll wheel + this.wheelActionTimeout = null; + this.resetScrollTimeout = null; + this.scrollX = 0; + this.scrollY = 0; + + // Used in panning zoomed images + this.moveStartX = 0; + this.moveStartY = 0; + this.moveStartOffsetX = 0; + this.moveStartOffsetY = 0; + + // Used to swipe + this.swipeStartX = 0; + this.swipeStartY = 0; + this.swipeEndX = 0; + this.swipeEndY = 0; + + // Used to pinch + this.pinchTouchList = null; + this.pinchDistance = 0; + + // Used to differentiate between images with identical src + this.keyCounter = 0; + + // Used to detect a move when all src's remain unchanged (four or more of the same image in a row) + this.moveRequested = false; + + if (!this.props.animationDisabled) { + // Make opening animation play + this.setState({ isClosing: false }); + } + } + + componentDidMount() { + // Prevents cross-origin errors when using a cross-origin iframe + this.windowContext = getHighestSafeWindowContext(); + + this.listeners = { + resize: this.handleWindowResize, + mouseup: this.handleMouseUp, + touchend: this.handleTouchEnd, + touchcancel: this.handleTouchEnd, + pointerdown: this.handlePointerEvent, + pointermove: this.handlePointerEvent, + pointerup: this.handlePointerEvent, + pointercancel: this.handlePointerEvent, + }; + Object.keys(this.listeners).forEach(type => { + this.windowContext.addEventListener(type, this.listeners[type]); + }); + + this.loadAllImages(); + } + + componentWillReceiveProps(nextProps) { + // Iterate through the source types for prevProps and nextProps to + // determine if any of the sources changed + let sourcesChanged = false; + const prevSrcDict = {}; + const nextSrcDict = {}; + this.getSrcTypes().forEach(srcType => { + if (this.props[srcType.name] !== nextProps[srcType.name]) { + sourcesChanged = true; + + prevSrcDict[this.props[srcType.name]] = true; + nextSrcDict[nextProps[srcType.name]] = true; + } + }); + + if (sourcesChanged || this.moveRequested) { + // Reset the loaded state for images not rendered next + Object.keys(prevSrcDict).forEach(prevSrc => { + if (!(prevSrc in nextSrcDict) && prevSrc in this.imageCache) { + this.imageCache[prevSrc].loaded = false; + } + }); + + this.moveRequested = false; + + // Load any new images + this.loadAllImages(nextProps); + } + } + + shouldComponentUpdate() { + // Wait for move... + return !this.moveRequested; + } + + componentWillUnmount() { + this.didUnmount = true; + Object.keys(this.listeners).forEach(type => { + this.windowContext.removeEventListener(type, this.listeners[type]); + }); + this.timeouts.forEach(tid => clearTimeout(tid)); + } + + setTimeout(func, time) { + const id = setTimeout(() => { + this.timeouts = this.timeouts.filter(tid => tid !== id); + func(); + }, time); + this.timeouts.push(id); + return id; + } + + setPreventInnerClose() { + if (this.preventInnerCloseTimeout) { + this.clearTimeout(this.preventInnerCloseTimeout); + } + this.preventInnerClose = true; + this.preventInnerCloseTimeout = this.setTimeout(() => { + this.preventInnerClose = false; + this.preventInnerCloseTimeout = null; + }, 100); + } + + // Get info for the best suited image to display with the given srcType + getBestImageForType(srcType) { + let imageSrc = this.props[srcType]; + let fitSizes = {}; + + if (this.isImageLoaded(imageSrc)) { + // Use full-size image if available + fitSizes = this.getFitSizes( + this.imageCache[imageSrc].width, + this.imageCache[imageSrc].height + ); + } else if (this.isImageLoaded(this.props[`${srcType}Thumbnail`])) { + // Fall back to using thumbnail if the image has not been loaded + imageSrc = this.props[`${srcType}Thumbnail`]; + fitSizes = this.getFitSizes( + this.imageCache[imageSrc].width, + this.imageCache[imageSrc].height, + true + ); + } else { + return null; + } + + return { + src: imageSrc, + height: this.imageCache[imageSrc].height, + width: this.imageCache[imageSrc].width, + targetHeight: fitSizes.height, + targetWidth: fitSizes.width, + }; + } + + // Get sizing for when an image is larger than the window + getFitSizes(width, height, stretch) { + const boxSize = this.getLightboxRect(); + let maxHeight = boxSize.height - (this.props.imagePadding * 2); + let maxWidth = boxSize.width - (this.props.imagePadding * 2); + + if (!stretch) { + maxHeight = Math.min(maxHeight, height); + maxWidth = Math.min(maxWidth, width); + } + + const maxRatio = maxWidth / maxHeight; + const srcRatio = width / height; + + if (maxRatio > srcRatio) { + // height is the constraining dimension of the photo + return { + width: (width * maxHeight) / height, + height: maxHeight, + }; + } + + return { + width: maxWidth, + height: (height * maxWidth) / width, + }; + } + + getMaxOffsets(zoomLevel = this.state.zoomLevel) { + const currentImageInfo = this.getBestImageForType('mainSrc'); + if (currentImageInfo === null) { + return { + maxX: 0, + minX: 0, + maxY: 0, + minY: 0 + }; + } + + const boxSize = this.getLightboxRect(); + const zoomMultiplier = this.getZoomMultiplier(zoomLevel); + + let maxX = 0; + if ((zoomMultiplier * currentImageInfo.width) - boxSize.width < 0) { + // if there is still blank space in the X dimension, don't limit except to the opposite edge + maxX = (boxSize.width - (zoomMultiplier * currentImageInfo.width)) / 2; + } else { + maxX = ((zoomMultiplier * currentImageInfo.width) - boxSize.width) / 2; + } + + let maxY = 0; + if ((zoomMultiplier * currentImageInfo.height) - boxSize.height < 0) { + // if there is still blank space in the Y dimension, don't limit except to the opposite edge + maxY = (boxSize.height - (zoomMultiplier * currentImageInfo.height)) / 2; + } else { + maxY = ((zoomMultiplier * currentImageInfo.height) - boxSize.height) / 2; + } + + return { + maxX, + maxY, + minX: -1 * maxX, + minY: -1 * maxY, + }; + } + + // Get image src types + getSrcTypes() { + return [ + { + name: 'mainSrc', + keyEnding: `i${this.keyCounter}`, + }, + { + name: 'mainSrcThumbnail', + keyEnding: `t${this.keyCounter}`, + }, + { + name: 'nextSrc', + keyEnding: `i${this.keyCounter + 1}`, + }, + { + name: 'nextSrcThumbnail', + keyEnding: `t${this.keyCounter + 1}`, + }, + { + name: 'prevSrc', + keyEnding: `i${this.keyCounter - 1}`, + }, + { + name: 'prevSrcThumbnail', + keyEnding: `t${this.keyCounter - 1}`, + }, + ]; + } + + /** + * Get sizing when the image is scaled + */ + getZoomMultiplier(zoomLevel = this.state.zoomLevel) { + return ZOOM_RATIO ** zoomLevel; + } + + /** + * Get the size of the lightbox in pixels + */ + getLightboxRect() { + if (this.outerEl) { + return this.outerEl.getBoundingClientRect(); + } + + return { + width: getWindowWidth(), + height: getWindowHeight(), + top: 0, + right: 0, + bottom: 0, + left: 0, + }; + } + + clearTimeout(id) { + this.timeouts = this.timeouts.filter(tid => tid !== id); + clearTimeout(id); + } + + // Change zoom level + changeZoom(zoomLevel, clientX, clientY) { + // Ignore if zoom disabled + if (!this.props.enableZoom) { + return; + } + + // Constrain zoom level to the set bounds + const nextZoomLevel = Math.max( + MIN_ZOOM_LEVEL, + Math.min(MAX_ZOOM_LEVEL, zoomLevel) + ); + + // Ignore requests that don't change the zoom level + if (nextZoomLevel === this.state.zoomLevel) { + return; + } else if (nextZoomLevel === MIN_ZOOM_LEVEL) { + // Snap back to center if zoomed all the way out + this.setState({ + zoomLevel: nextZoomLevel, + offsetX: 0, + offsetY: 0, + }); + + return; + } + + const imageBaseSize = this.getBestImageForType('mainSrc'); + if (imageBaseSize === null) { + return; + } + + const currentZoomMultiplier = this.getZoomMultiplier(); + const nextZoomMultiplier = this.getZoomMultiplier(nextZoomLevel); + + // Default to the center of the image to zoom when no mouse position specified + const boxRect = this.getLightboxRect(); + const pointerX = + typeof clientX !== 'undefined' + ? clientX - boxRect.left + : boxRect.width / 2; + const pointerY = + typeof clientY !== 'undefined' + ? clientY - boxRect.top + : boxRect.height / 2; + + const currentImageOffsetX = + (boxRect.width - (imageBaseSize.width * currentZoomMultiplier)) / 2; + const currentImageOffsetY = + (boxRect.height - (imageBaseSize.height * currentZoomMultiplier)) / 2; + + const currentImageRealOffsetX = currentImageOffsetX - this.state.offsetX; + const currentImageRealOffsetY = currentImageOffsetY - this.state.offsetY; + + const currentPointerXRelativeToImage = + (pointerX - currentImageRealOffsetX) / currentZoomMultiplier; + const currentPointerYRelativeToImage = + (pointerY - currentImageRealOffsetY) / currentZoomMultiplier; + + const nextImageRealOffsetX = + pointerX - (currentPointerXRelativeToImage * nextZoomMultiplier); + const nextImageRealOffsetY = + pointerY - (currentPointerYRelativeToImage * nextZoomMultiplier); + + const nextImageOffsetX = + (boxRect.width - (imageBaseSize.width * nextZoomMultiplier)) / 2; + const nextImageOffsetY = + (boxRect.height - (imageBaseSize.height * nextZoomMultiplier)) / 2; + + let nextOffsetX = nextImageOffsetX - nextImageRealOffsetX; + let nextOffsetY = nextImageOffsetY - nextImageRealOffsetY; + + // When zooming out, limit the offset so things don't get left askew + if (this.currentAction !== ACTION_PINCH) { + const maxOffsets = this.getMaxOffsets(); + if (this.state.zoomLevel > nextZoomLevel) { + nextOffsetX = Math.max( + maxOffsets.minX, + Math.min(maxOffsets.maxX, nextOffsetX) + ); + nextOffsetY = Math.max( + maxOffsets.minY, + Math.min(maxOffsets.maxY, nextOffsetY) + ); + } + } + + this.setState({ + zoomLevel: nextZoomLevel, + offsetX: nextOffsetX, + offsetY: nextOffsetY, + }); + } + + closeIfClickInner(event) { + if ( + !this.preventInnerClose && + event.target.className.search(/\bril-inner\b/) > -1 + ) { + this.requestClose(event); + } + } + + /** + * Handle user keyboard actions + */ + handleKeyInput(event) { + event.stopPropagation(); + + // Ignore key input during animations + if (this.isAnimating()) { + return; + } + + // Allow slightly faster navigation through the images when user presses keys repeatedly + if (event.type === 'keyup') { + this.lastKeyDownTime -= this.props.keyRepeatKeyupBonus; + return; + } + + const keyCode = event.which || event.keyCode; + + // Ignore key presses that happen too close to each other (when rapid fire key pressing or holding down the key) + // But allow it if it's a lightbox closing action + const currentTime = new Date(); + if ( + currentTime.getTime() - this.lastKeyDownTime < + this.props.keyRepeatLimit && + keyCode !== KEYS.ESC + ) { + return; + } + this.lastKeyDownTime = currentTime.getTime(); + + switch (keyCode) { + // ESC key closes the lightbox + case KEYS.ESC: + event.preventDefault(); + this.requestClose(event); + break; + + // Left arrow key moves to previous image + case KEYS.LEFT_ARROW: + if (!this.props.prevSrc) { + return; + } + + event.preventDefault(); + this.keyPressed = true; + this.requestMovePrev(event); + break; + + // Right arrow key moves to next image + case KEYS.RIGHT_ARROW: + if (!this.props.nextSrc) { + return; + } + + event.preventDefault(); + this.keyPressed = true; + this.requestMoveNext(event); + break; + + default: + } + } + + /** + * Handle a mouse wheel event over the lightbox container + */ + handleOuterMousewheel(event) { + // Prevent scrolling of the background + event.preventDefault(); + event.stopPropagation(); + + const xThreshold = WHEEL_MOVE_X_THRESHOLD; + let actionDelay = 0; + const imageMoveDelay = 500; + + this.clearTimeout(this.resetScrollTimeout); + this.resetScrollTimeout = this.setTimeout(() => { + this.scrollX = 0; + this.scrollY = 0; + }, 300); + + // Prevent rapid-fire zoom behavior + if (this.wheelActionTimeout !== null || this.isAnimating()) { + return; + } + + if (Math.abs(event.deltaY) < Math.abs(event.deltaX)) { + // handle horizontal scrolls with image moves + this.scrollY = 0; + this.scrollX += event.deltaX; + + const bigLeapX = xThreshold / 2; + // If the scroll amount has accumulated sufficiently, or a large leap was taken + if (this.scrollX >= xThreshold || event.deltaX >= bigLeapX) { + // Scroll right moves to next + this.requestMoveNext(event); + actionDelay = imageMoveDelay; + this.scrollX = 0; + } else if ( + this.scrollX <= -1 * xThreshold || + event.deltaX <= -1 * bigLeapX + ) { + // Scroll left moves to previous + this.requestMovePrev(event); + actionDelay = imageMoveDelay; + this.scrollX = 0; + } + } + + // Allow successive actions after the set delay + if (actionDelay !== 0) { + this.wheelActionTimeout = this.setTimeout(() => { + this.wheelActionTimeout = null; + }, actionDelay); + } + } + + handleImageMouseWheel(event) { + event.preventDefault(); + const yThreshold = WHEEL_MOVE_Y_THRESHOLD; + + if (Math.abs(event.deltaY) >= Math.abs(event.deltaX)) { + event.stopPropagation(); + // If the vertical scroll amount was large enough, perform a zoom + if (Math.abs(event.deltaY) < yThreshold) { + return; + } + + this.scrollX = 0; + this.scrollY += event.deltaY; + + this.changeZoom( + this.state.zoomLevel - event.deltaY, + event.clientX, + event.clientY + ); + } + } + + /** + * Handle a double click on the current image + */ + handleImageDoubleClick(event) { + if (this.state.zoomLevel > MIN_ZOOM_LEVEL) { + // A double click when zoomed in zooms all the way out + this.changeZoom(MIN_ZOOM_LEVEL, event.clientX, event.clientY); + } else { + // A double click when zoomed all the way out zooms in + this.changeZoom( + this.state.zoomLevel + ZOOM_BUTTON_INCREMENT_SIZE, + event.clientX, + event.clientY + ); + } + } + + shouldHandleEvent(source) { + if (this.eventsSource === source) { + return true; + } + if (this.eventsSource === SOURCE_ANY) { + this.eventsSource = source; + return true; + } + switch (source) { + case SOURCE_MOUSE: + return false; + case SOURCE_TOUCH: + this.eventsSource = SOURCE_TOUCH; + this.filterPointersBySource(); + return true; + case SOURCE_POINTER: + if (this.eventsSource === SOURCE_MOUSE) { + this.eventsSource = SOURCE_POINTER; + this.filterPointersBySource(); + return true; + } + return false; + default: + return false; + } + } + + addPointer(pointer) { + this.pointerList.push(pointer); + } + + removePointer(pointer) { + this.pointerList = this.pointerList.filter(({ id }) => id !== pointer.id); + } + + filterPointersBySource() { + this.pointerList = this.pointerList.filter( + ({ source }) => source === this.eventsSource + ); + } + + handleMouseDown(event) { + if ( + this.shouldHandleEvent(SOURCE_MOUSE) && + ReactImageLightbox.isTargetMatchImage(event.target) + ) { + this.addPointer(ReactImageLightbox.parseMouseEvent(event)); + this.multiPointerStart(event); + } + } + + handleMouseMove(event) { + if (this.shouldHandleEvent(SOURCE_MOUSE)) { + this.multiPointerMove(event, [ReactImageLightbox.parseMouseEvent(event)]); + } + } + + handleMouseUp(event) { + if (this.shouldHandleEvent(SOURCE_MOUSE)) { + this.removePointer(ReactImageLightbox.parseMouseEvent(event)); + this.multiPointerEnd(event); + } + } + + handlePointerEvent(event) { + if (this.shouldHandleEvent(SOURCE_POINTER)) { + switch (event.type) { + case 'pointerdown': + if (ReactImageLightbox.isTargetMatchImage(event.target)) { + this.addPointer(ReactImageLightbox.parsePointerEvent(event)); + this.multiPointerStart(event); + } + break; + case 'pointermove': + this.multiPointerMove(event, [ + ReactImageLightbox.parsePointerEvent(event), + ]); + break; + case 'pointerup': + case 'pointercancel': + this.removePointer(ReactImageLightbox.parsePointerEvent(event)); + this.multiPointerEnd(event); + break; + default: + break; + } + } + } + + handleTouchStart(event) { + if ( + this.shouldHandleEvent(SOURCE_TOUCH) && + ReactImageLightbox.isTargetMatchImage(event.target) + ) { + [].forEach.call(event.changedTouches, eventTouch => + this.addPointer(ReactImageLightbox.parseTouchPointer(eventTouch)) + ); + this.multiPointerStart(event); + } + } + + handleTouchMove(event) { + if (this.shouldHandleEvent(SOURCE_TOUCH)) { + this.multiPointerMove( + event, + [].map.call(event.changedTouches, eventTouch => + ReactImageLightbox.parseTouchPointer(eventTouch) + ) + ); + } + } + + handleTouchEnd(event) { + if (this.shouldHandleEvent(SOURCE_TOUCH)) { + [].map.call(event.changedTouches, touch => + this.removePointer(ReactImageLightbox.parseTouchPointer(touch)) + ); + this.multiPointerEnd(event); + } + } + + decideMoveOrSwipe(pointer) { + if (this.state.zoomLevel <= MIN_ZOOM_LEVEL) { + this.handleSwipeStart(pointer); + } else { + this.handleMoveStart(pointer); + } + } + + multiPointerStart(event) { + this.handleEnd(null); + switch (this.pointerList.length) { + case 1: { + event.preventDefault(); + this.decideMoveOrSwipe(this.pointerList[0]); + break; + } + case 2: { + event.preventDefault(); + this.handlePinchStart(this.pointerList); + break; + } + default: + break; + } + } + + multiPointerMove(event, pointerList) { + switch (this.currentAction) { + case ACTION_MOVE: { + event.preventDefault(); + this.handleMove(pointerList[0]); + break; + } + case ACTION_SWIPE: { + event.preventDefault(); + this.handleSwipe(pointerList[0]); + break; + } + case ACTION_PINCH: { + event.preventDefault(); + this.handlePinch(pointerList); + break; + } + default: + break; + } + } + + multiPointerEnd(event) { + if (this.currentAction !== ACTION_NONE) { + this.setPreventInnerClose(); + this.handleEnd(event); + } + switch (this.pointerList.length) { + case 0: { + this.eventsSource = SOURCE_ANY; + break; + } + case 1: { + event.preventDefault(); + this.decideMoveOrSwipe(this.pointerList[0]); + break; + } + case 2: { + event.preventDefault(); + this.handlePinchStart(this.pointerList); + break; + } + default: + break; + } + } + + handleEnd(event) { + switch (this.currentAction) { + case ACTION_MOVE: + this.handleMoveEnd(event); + break; + case ACTION_SWIPE: + this.handleSwipeEnd(event); + break; + case ACTION_PINCH: + this.handlePinchEnd(event); + break; + default: + break; + } + } + + // Handle move start over the lightbox container + // This happens: + // - On a mouseDown event + // - On a touchstart event + handleMoveStart({ x: clientX, y: clientY }) { + if (!this.props.enableZoom) { + return; + } + this.currentAction = ACTION_MOVE; + this.moveStartX = clientX; + this.moveStartY = clientY; + this.moveStartOffsetX = this.state.offsetX; + this.moveStartOffsetY = this.state.offsetY; + } + + // Handle dragging over the lightbox container + // This happens: + // - After a mouseDown and before a mouseUp event + // - After a touchstart and before a touchend event + handleMove({ x: clientX, y: clientY }) { + const newOffsetX = (this.moveStartX - clientX) + this.moveStartOffsetX; + const newOffsetY = (this.moveStartY - clientY) + this.moveStartOffsetY; + if ( + this.state.offsetX !== newOffsetX || + this.state.offsetY !== newOffsetY + ) { + this.setState({ + offsetX: newOffsetX, + offsetY: newOffsetY, + }); + } + } + + handleMoveEnd() { + this.currentAction = ACTION_NONE; + this.moveStartX = 0; + this.moveStartY = 0; + this.moveStartOffsetX = 0; + this.moveStartOffsetY = 0; + // Snap image back into frame if outside max offset range + const maxOffsets = this.getMaxOffsets(); + const nextOffsetX = Math.max( + maxOffsets.minX, + Math.min(maxOffsets.maxX, this.state.offsetX) + ); + const nextOffsetY = Math.max( + maxOffsets.minY, + Math.min(maxOffsets.maxY, this.state.offsetY) + ); + if ( + nextOffsetX !== this.state.offsetX || + nextOffsetY !== this.state.offsetY + ) { + this.setState({ + offsetX: nextOffsetX, + offsetY: nextOffsetY, + shouldAnimate: true, + }); + this.setTimeout(() => { + this.setState({ shouldAnimate: false }); + }, this.props.animationDuration); + } + } + + handleSwipeStart({ x: clientX, y: clientY }) { + this.currentAction = ACTION_SWIPE; + this.swipeStartX = clientX; + this.swipeStartY = clientY; + this.swipeEndX = clientX; + this.swipeEndY = clientY; + } + + handleSwipe({ x: clientX, y: clientY }) { + this.swipeEndX = clientX; + this.swipeEndY = clientY; + } + + handleSwipeEnd(event) { + const xDiff = this.swipeEndX - this.swipeStartX; + const xDiffAbs = Math.abs(xDiff); + const yDiffAbs = Math.abs(this.swipeEndY - this.swipeStartY); + + this.currentAction = ACTION_NONE; + this.swipeStartX = 0; + this.swipeStartY = 0; + this.swipeEndX = 0; + this.swipeEndY = 0; + + if (!event || this.isAnimating() || xDiffAbs < yDiffAbs * 1.5) { + return; + } + + if (xDiffAbs < MIN_SWIPE_DISTANCE) { + const boxRect = this.getLightboxRect(); + if (xDiffAbs < boxRect.width / 4) { + return; + } + } + + if (xDiff > 0 && this.props.prevSrc) { + event.preventDefault(); + this.requestMovePrev(); + } else if (xDiff < 0 && this.props.nextSrc) { + event.preventDefault(); + this.requestMoveNext(); + } + } + + calculatePinchDistance([a, b] = this.pinchTouchList) { + return Math.sqrt(((a.x - b.x) ** 2) + ((a.y - b.y) ** 2)); + } + + calculatePinchCenter([a, b] = this.pinchTouchList) { + return { + x: a.x - ((a.x - b.x) / 2), + y: a.y - ((a.y - b.y) / 2), + }; + } + + handlePinchStart(pointerList) { + if (!this.props.enableZoom) { + return; + } + this.currentAction = ACTION_PINCH; + this.pinchTouchList = pointerList.map(({ id, x, y }) => ({ id, x, y })); + this.pinchDistance = this.calculatePinchDistance(); + } + + handlePinch(pointerList) { + this.pinchTouchList = this.pinchTouchList.map(oldPointer => { + for (let i = 0; i < pointerList.length; i += 1) { + if (pointerList[i].id === oldPointer.id) { + return pointerList[i]; + } + } + + return oldPointer; + }); + + const newDistance = this.calculatePinchDistance(); + + const zoomLevel = (this.state.zoomLevel + newDistance) - this.pinchDistance; + + this.pinchDistance = newDistance; + const { x: clientX, y: clientY } = this.calculatePinchCenter( + this.pinchTouchList + ); + this.changeZoom(zoomLevel, clientX, clientY); + } + + handlePinchEnd() { + this.currentAction = ACTION_NONE; + this.pinchTouchList = null; + this.pinchDistance = 0; + } + + // Handle the window resize event + handleWindowResize() { + this.clearTimeout(this.resizeTimeout); + this.resizeTimeout = this.setTimeout(this.forceUpdate.bind(this), 100); + } + + handleZoomInButtonClick() { + this.changeZoom(this.state.zoomLevel + ZOOM_BUTTON_INCREMENT_SIZE); + } + + handleZoomOutButtonClick() { + this.changeZoom(this.state.zoomLevel - ZOOM_BUTTON_INCREMENT_SIZE); + } + + handleCaptionMousewheel(event) { + event.stopPropagation(); + + if (!this.caption) { + return; + } + + const { height } = this.caption.getBoundingClientRect(); + const { scrollHeight, scrollTop } = this.caption; + if ( + (event.deltaY > 0 && height + scrollTop >= scrollHeight) || + (event.deltaY < 0 && scrollTop <= 0) + ) { + event.preventDefault(); + } + } + + // Detach key and mouse input events + isAnimating() { + return this.state.shouldAnimate || this.state.isClosing; + } + + // Check if image is loaded + isImageLoaded(imageSrc) { + return ( + imageSrc && + imageSrc in this.imageCache && + this.imageCache[imageSrc].loaded + ); + } + + // Load image from src and call callback with image width and height on load + loadImage(srcType, imageSrc, done) { + // Return the image info if it is already cached + if (this.isImageLoaded(imageSrc)) { + this.setTimeout(() => { + done(); + }, 1); + return; + } + + const inMemoryImage = new global.Image(); + + if (this.props.imageCrossOrigin) { + inMemoryImage.crossOrigin = this.props.imageCrossOrigin; + } + + inMemoryImage.onerror = errorEvent => { + this.props.onImageLoadError(imageSrc, srcType, errorEvent); + + // failed to load so set the state loadErrorStatus + this.setState(prevState => ({ + loadErrorStatus: { ...prevState.loadErrorStatus, [srcType]: true }, + })); + + done(errorEvent); + }; + + inMemoryImage.onload = () => { + this.props.onImageLoad(imageSrc, srcType, inMemoryImage); + + this.imageCache[imageSrc] = { + loaded: true, + width: inMemoryImage.width, + height: inMemoryImage.height, + }; + + done(); + }; + + inMemoryImage.src = imageSrc; + } + + // Load all images and their thumbnails + loadAllImages(props = this.props) { + const generateLoadDoneCallback = (srcType, imageSrc) => err => { + // Give up showing image on error + if (err) { + return; + } + + // Don't rerender if the src is not the same as when the load started + // or if the component has unmounted + if (this.props[srcType] !== imageSrc || this.didUnmount) { + return; + } + + // Force rerender with the new image + this.forceUpdate(); + }; + + // Load the images + this.getSrcTypes().forEach(srcType => { + const type = srcType.name; + + // there is no error when we try to load it initially + if (props[type] && this.state.loadErrorStatus[type]) { + this.setState(prevState => ({ + loadErrorStatus: { ...prevState.loadErrorStatus, [type]: false }, + })); + } + + // Load unloaded images + if (props[type] && !this.isImageLoaded(props[type])) { + this.loadImage( + type, + props[type], + generateLoadDoneCallback(type, props[type]) + ); + } + }); + } + + // Request that the lightbox be closed + requestClose(event) { + // Call the parent close request + const closeLightbox = () => this.props.onCloseRequest(event); + + if ( + this.props.animationDisabled || + (event.type === 'keydown' && !this.props.animationOnKeyInput) + ) { + // No animation + closeLightbox(); + return; + } + + // With animation + // Start closing animation + this.setState({ isClosing: true }); + + // Perform the actual closing at the end of the animation + this.setTimeout(closeLightbox, this.props.animationDuration); + } + + requestMove(direction, event) { + // Reset the zoom level on image move + const nextState = { + zoomLevel: MIN_ZOOM_LEVEL, + offsetX: 0, + offsetY: 0, + }; + + // Enable animated states + if ( + !this.props.animationDisabled && + (!this.keyPressed || this.props.animationOnKeyInput) + ) { + nextState.shouldAnimate = true; + this.setTimeout( + () => this.setState({ shouldAnimate: false }), + this.props.animationDuration + ); + } + this.keyPressed = false; + + this.moveRequested = true; + + if (direction === 'prev') { + this.keyCounter -= 1; + this.setState(nextState); + this.props.onMovePrevRequest(event); + } else { + this.keyCounter += 1; + this.setState(nextState); + this.props.onMoveNextRequest(event); + } + } + + // Request to transition to the next image + requestMoveNext(event) { + this.requestMove('next', event); + } + + // Request to transition to the previous image + requestMovePrev(event) { + this.requestMove('prev', event); + } + + render() { + const { + animationDisabled, + animationDuration, + clickOutsideToClose, + discourageDownloads, + enableZoom, + imageTitle, + nextSrc, + prevSrc, + toolbarButtons, + reactModalStyle, + onAfterOpen, + imageCrossOrigin, + reactModalProps, + } = this.props; + const { + zoomLevel, + offsetX, + offsetY, + isClosing, + loadErrorStatus, + } = this.state; + + const boxSize = this.getLightboxRect(); + let transitionStyle = {}; + + // Transition settings for sliding animations + if (!animationDisabled && this.isAnimating()) { + transitionStyle = { + ...transitionStyle, + transition: `transform ${animationDuration}ms`, + }; + } + + // Key endings to differentiate between images with the same src + const keyEndings = {}; + this.getSrcTypes().forEach(({ name, keyEnding }) => { + keyEndings[name] = keyEnding; + }); + + // Images to be displayed + const images = []; + const addImage = (srcType, imageClass, transforms) => { + // Ignore types that have no source defined for their full size image + if (!this.props[srcType]) { + return; + } + const bestImageInfo = this.getBestImageForType(srcType); + + const imageStyle = { + ...transitionStyle, + ...ReactImageLightbox.getTransform({ + ...transforms, + ...bestImageInfo, + }), + }; + + if (zoomLevel > MIN_ZOOM_LEVEL) { + imageStyle.cursor = 'move'; + } + + // support IE 9 and 11 + const hasTrueValue = object => + Object.keys(object).some(key => object[key]); + + // when error on one of the loads then push custom error stuff + if (bestImageInfo === null && hasTrueValue(loadErrorStatus)) { + images.push( + <div + className={`${imageClass} ril__image ril-errored`} + style={imageStyle} + key={this.props[srcType] + keyEndings[srcType]} + > + <div className="ril__errorContainer"> + {this.props.imageLoadErrorMessage} + </div> + </div> + ); + + return; + } else if (bestImageInfo === null) { + const loadingIcon = ( + <div className="ril-loading-circle ril__loadingCircle ril__loadingContainer__icon"> + {[...new Array(12)].map((_, index) => ( + <div + // eslint-disable-next-line react/no-array-index-key + key={index} + className="ril-loading-circle-point ril__loadingCirclePoint" + /> + ))} + </div> + ); + + // Fall back to loading icon if the thumbnail has not been loaded + images.push( + <div + className={`${imageClass} ril__image ril-not-loaded`} + style={imageStyle} + key={this.props[srcType] + keyEndings[srcType]} + > + <div className="ril__loadingContainer">{loadingIcon}</div> + </div> + ); + + return; + } + + const imageSrc = bestImageInfo.src; + if (discourageDownloads) { + imageStyle.backgroundImage = `url('${imageSrc}')`; + images.push( + <div + className={`${imageClass} ril__image ril__imageDiscourager`} + onDoubleClick={this.handleImageDoubleClick} + onWheel={this.handleImageMouseWheel} + style={imageStyle} + key={imageSrc + keyEndings[srcType]} + > + <div className="ril-download-blocker ril__downloadBlocker" /> + </div> + ); + } else { + images.push( + <img + {...(imageCrossOrigin ? { crossOrigin: imageCrossOrigin } : {})} + className={`${imageClass} ril__image`} + onDoubleClick={this.handleImageDoubleClick} + onWheel={this.handleImageMouseWheel} + onDragStart={e => e.preventDefault()} + style={imageStyle} + src={imageSrc} + key={imageSrc + keyEndings[srcType]} + alt={ + typeof imageTitle === 'string' ? imageTitle : translate('Image') + } + draggable={false} + /> + ); + } + }; + + const zoomMultiplier = this.getZoomMultiplier(); + // Next Image (displayed on the right) + addImage('nextSrc', 'ril-image-next ril__imageNext', { + x: boxSize.width, + }); + // Main Image + addImage('mainSrc', 'ril-image-current', { + x: -1 * offsetX, + y: -1 * offsetY, + zoom: zoomMultiplier, + }); + // Previous Image (displayed on the left) + addImage('prevSrc', 'ril-image-prev ril__imagePrev', { + x: -1 * boxSize.width, + }); + + const modalStyle = { + overlay: { + zIndex: 1000, + backgroundColor: 'transparent', + ...reactModalStyle.overlay, // Allow style overrides via props + }, + content: { + backgroundColor: 'transparent', + overflow: 'hidden', // Needed, otherwise keyboard shortcuts scroll the page + border: 'none', + borderRadius: 0, + padding: 0, + top: 0, + left: 0, + right: 0, + bottom: 0, + ...reactModalStyle.content, // Allow style overrides via props + }, + }; + + return ( + <Modal + isOpen + onRequestClose={clickOutsideToClose ? this.requestClose : undefined} + onAfterOpen={() => { + // Focus on the div with key handlers + if (this.outerEl) { + this.outerEl.focus(); + } + + onAfterOpen(); + }} + style={modalStyle} + contentLabel={translate('Lightbox')} + appElement={ + typeof global.window !== 'undefined' + ? global.window.document.body + : undefined + } + {...reactModalProps} + > + <div // eslint-disable-line jsx-a11y/no-static-element-interactions + // Floating modal with closing animations + className={`ril-outer ril__outer ril__outerAnimating ${ + this.props.wrapperClassName + } ${isClosing ? 'ril-closing ril__outerClosing' : ''}`} + style={{ + transition: `opacity ${animationDuration}ms`, + animationDuration: `${animationDuration}ms`, + animationDirection: isClosing ? 'normal' : 'reverse', + }} + ref={el => { + this.outerEl = el; + }} + onWheel={this.handleOuterMousewheel} + onMouseMove={this.handleMouseMove} + onMouseDown={this.handleMouseDown} + onTouchStart={this.handleTouchStart} + onTouchMove={this.handleTouchMove} + tabIndex="-1" // Enables key handlers on div + onKeyDown={this.handleKeyInput} + onKeyUp={this.handleKeyInput} + > + <div // eslint-disable-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events + // Image holder + className="ril-inner ril__inner" + onClick={clickOutsideToClose ? this.closeIfClickInner : undefined} + > + {images} + </div> + + {prevSrc && ( + <button // Move to previous image button + type="button" + className="ril-prev-button ril__navButtons ril__navButtonPrev" + key="prev" + aria-label={this.props.prevLabel} + onClick={!this.isAnimating() ? this.requestMovePrev : undefined} // Ignore clicks during animation + /> + )} + + {nextSrc && ( + <button // Move to next image button + type="button" + className="ril-next-button ril__navButtons ril__navButtonNext" + key="next" + aria-label={this.props.nextLabel} + onClick={!this.isAnimating() ? this.requestMoveNext : undefined} // Ignore clicks during animation + /> + )} + + <div // Lightbox toolbar + className="ril-toolbar ril__toolbar" + > + <ul className="ril-toolbar-left ril__toolbarSide ril__toolbarLeftSide"> + <li className="ril-toolbar__item ril__toolbarItem"> + <span className="ril-toolbar__item__child ril__toolbarItemChild"> + {imageTitle} + </span> + </li> + </ul> + + <ul className="ril-toolbar-right ril__toolbarSide ril__toolbarRightSide"> + {toolbarButtons && + toolbarButtons.map((button, i) => ( + <li + key={`button_${i + 1}`} + className="ril-toolbar__item ril__toolbarItem" + > + {button} + </li> + ))} + + {enableZoom && ( + <li className="ril-toolbar__item ril__toolbarItem"> + <button // Lightbox zoom in button + type="button" + key="zoom-in" + aria-label={this.props.zoomInLabel} + className={[ + 'ril-zoom-in', + 'ril__toolbarItemChild', + 'ril__builtinButton', + 'ril__zoomInButton', + ...(zoomLevel === MAX_ZOOM_LEVEL + ? ['ril__builtinButtonDisabled'] + : []), + ].join(' ')} + disabled={ + this.isAnimating() || zoomLevel === MAX_ZOOM_LEVEL + } + onClick={ + !this.isAnimating() && zoomLevel !== MAX_ZOOM_LEVEL + ? this.handleZoomInButtonClick + : undefined + } + /> + </li> + )} + + {enableZoom && ( + <li className="ril-toolbar__item ril__toolbarItem"> + <button // Lightbox zoom out button + type="button" + key="zoom-out" + aria-label={this.props.zoomOutLabel} + className={[ + 'ril-zoom-out', + 'ril__toolbarItemChild', + 'ril__builtinButton', + 'ril__zoomOutButton', + ...(zoomLevel === MIN_ZOOM_LEVEL + ? ['ril__builtinButtonDisabled'] + : []), + ].join(' ')} + disabled={ + this.isAnimating() || zoomLevel === MIN_ZOOM_LEVEL + } + onClick={ + !this.isAnimating() && zoomLevel !== MIN_ZOOM_LEVEL + ? this.handleZoomOutButtonClick + : undefined + } + /> + </li> + )} + + <li className="ril-toolbar__item ril__toolbarItem"> + <button // Lightbox close button + type="button" + key="close" + aria-label={this.props.closeLabel} + className="ril-close ril-toolbar__item__child ril__toolbarItemChild ril__builtinButton ril__closeButton" + onClick={!this.isAnimating() ? this.requestClose : undefined} // Ignore clicks during animation + /> + </li> + </ul> + </div> + + {this.props.imageCaption && ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + <div // Image caption + onWheel={this.handleCaptionMousewheel} + onMouseDown={event => event.stopPropagation()} + className="ril-caption ril__caption" + ref={el => { + this.caption = el; + }} + > + <div className="ril-caption-content ril__captionContent"> + {this.props.imageCaption} + </div> + </div> + )} + </div> + </Modal> + ); + } +} + +ReactImageLightbox.propTypes = { + //----------------------------- + // Image sources + //----------------------------- + + // Main display image url + mainSrc: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types + + // Previous display image url (displayed to the left) + // If left undefined, movePrev actions will not be performed, and the button not displayed + prevSrc: PropTypes.string, + + // Next display image url (displayed to the right) + // If left undefined, moveNext actions will not be performed, and the button not displayed + nextSrc: PropTypes.string, + + //----------------------------- + // Image thumbnail sources + //----------------------------- + + // Thumbnail image url corresponding to props.mainSrc + mainSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types + + // Thumbnail image url corresponding to props.prevSrc + prevSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types + + // Thumbnail image url corresponding to props.nextSrc + nextSrcThumbnail: PropTypes.string, // eslint-disable-line react/no-unused-prop-types + + //----------------------------- + // Event Handlers + //----------------------------- + + // Close window event + // Should change the parent state such that the lightbox is not rendered + onCloseRequest: PropTypes.func.isRequired, + + // Move to previous image event + // Should change the parent state such that props.prevSrc becomes props.mainSrc, + // props.mainSrc becomes props.nextSrc, etc. + onMovePrevRequest: PropTypes.func, + + // Move to next image event + // Should change the parent state such that props.nextSrc becomes props.mainSrc, + // props.mainSrc becomes props.prevSrc, etc. + onMoveNextRequest: PropTypes.func, + + // Called when an image fails to load + // (imageSrc: string, srcType: string, errorEvent: object): void + onImageLoadError: PropTypes.func, + + // Called when image successfully loads + onImageLoad: PropTypes.func, + + // Open window event + onAfterOpen: PropTypes.func, + + //----------------------------- + // Download discouragement settings + //----------------------------- + + // Enable download discouragement (prevents [right-click -> Save Image As...]) + discourageDownloads: PropTypes.bool, + + //----------------------------- + // Animation settings + //----------------------------- + + // Disable all animation + animationDisabled: PropTypes.bool, + + // Disable animation on actions performed with keyboard shortcuts + animationOnKeyInput: PropTypes.bool, + + // Animation duration (ms) + animationDuration: PropTypes.number, + + //----------------------------- + // Keyboard shortcut settings + //----------------------------- + + // Required interval of time (ms) between key actions + // (prevents excessively fast navigation of images) + keyRepeatLimit: PropTypes.number, + + // Amount of time (ms) restored after each keyup + // (makes rapid key presses slightly faster than holding down the key to navigate images) + keyRepeatKeyupBonus: PropTypes.number, + + //----------------------------- + // Image info + //----------------------------- + + // Image title + imageTitle: PropTypes.node, + + // Image caption + imageCaption: PropTypes.node, + + // Optional crossOrigin attribute + imageCrossOrigin: PropTypes.string, + + //----------------------------- + // Lightbox style + //----------------------------- + + // Set z-index style, etc., for the parent react-modal (format: https://github.com/reactjs/react-modal#styles ) + reactModalStyle: PropTypes.shape({}), + + // Padding (px) between the edge of the window and the lightbox + imagePadding: PropTypes.number, + + wrapperClassName: PropTypes.string, + + //----------------------------- + // Other + //----------------------------- + + // Array of custom toolbar buttons + toolbarButtons: PropTypes.arrayOf(PropTypes.node), + + // When true, clicks outside of the image close the lightbox + clickOutsideToClose: PropTypes.bool, + + // Set to false to disable zoom functionality and hide zoom buttons + enableZoom: PropTypes.bool, + + // Override props set on react-modal (https://github.com/reactjs/react-modal) + reactModalProps: PropTypes.shape({}), + + // Aria-labels + nextLabel: PropTypes.string, + prevLabel: PropTypes.string, + zoomInLabel: PropTypes.string, + zoomOutLabel: PropTypes.string, + closeLabel: PropTypes.string, + + imageLoadErrorMessage: PropTypes.node, +}; + +ReactImageLightbox.defaultProps = { + imageTitle: null, + imageCaption: null, + toolbarButtons: null, + reactModalProps: {}, + animationDisabled: false, + animationDuration: 300, + animationOnKeyInput: false, + clickOutsideToClose: true, + closeLabel: 'Close lightbox', + discourageDownloads: false, + enableZoom: true, + imagePadding: 10, + imageCrossOrigin: null, + keyRepeatKeyupBonus: 40, + keyRepeatLimit: 180, + mainSrcThumbnail: null, + nextLabel: 'Next image', + nextSrc: null, + nextSrcThumbnail: null, + onAfterOpen: () => {}, + onImageLoadError: () => {}, + onImageLoad: () => {}, + onMoveNextRequest: () => {}, + onMovePrevRequest: () => {}, + prevLabel: 'Previous image', + prevSrc: null, + prevSrcThumbnail: null, + reactModalStyle: {}, + wrapperClassName: '', + zoomInLabel: 'Zoom in', + zoomOutLabel: 'Zoom out', + imageLoadErrorMessage: 'This image failed to load', +}; + +export default ReactImageLightbox; diff --git a/front/odiparpack/app/components/ImageLightbox/constant.js b/front/odiparpack/app/components/ImageLightbox/constant.js new file mode 100644 index 0000000..c310b9e --- /dev/null +++ b/front/odiparpack/app/components/ImageLightbox/constant.js @@ -0,0 +1,39 @@ +// Min image zoom level +export const MIN_ZOOM_LEVEL = 0; + +// Max image zoom level +export const MAX_ZOOM_LEVEL = 300; + +// Size ratio between previous and next zoom levels +export const ZOOM_RATIO = 1.007; + +// How much to increase/decrease the zoom level when the zoom buttons are clicked +export const ZOOM_BUTTON_INCREMENT_SIZE = 100; + +// Used to judge the amount of horizontal scroll needed to initiate a image move +export const WHEEL_MOVE_X_THRESHOLD = 200; + +// Used to judge the amount of vertical scroll needed to initiate a zoom action +export const WHEEL_MOVE_Y_THRESHOLD = 1; + +export const KEYS = { + ESC: 27, + LEFT_ARROW: 37, + RIGHT_ARROW: 39, +}; + +// Actions +export const ACTION_NONE = 0; +export const ACTION_MOVE = 1; +export const ACTION_SWIPE = 2; +export const ACTION_PINCH = 3; +export const ACTION_ROTATE = 4; + +// Events source +export const SOURCE_ANY = 0; +export const SOURCE_MOUSE = 1; +export const SOURCE_TOUCH = 2; +export const SOURCE_POINTER = 3; + +// Minimal swipe distance +export const MIN_SWIPE_DISTANCE = 200; diff --git a/front/odiparpack/app/components/ImageLightbox/util.js b/front/odiparpack/app/components/ImageLightbox/util.js new file mode 100644 index 0000000..a76383a --- /dev/null +++ b/front/odiparpack/app/components/ImageLightbox/util.js @@ -0,0 +1,46 @@ +/** + * Placeholder for future translate functionality + */ +export function translate(str, replaceStrings = null) { + if (!str) { + return ''; + } + + let translated = str; + if (replaceStrings) { + Object.keys(replaceStrings).forEach(placeholder => { + translated = translated.replace(placeholder, replaceStrings[placeholder]); + }); + } + + return translated; +} + +export function getWindowWidth() { + return typeof global.window !== 'undefined' ? global.window.innerWidth : 0; +} + +export function getWindowHeight() { + return typeof global.window !== 'undefined' ? global.window.innerHeight : 0; +} + +// Get the highest window context that isn't cross-origin +// (When in an iframe) +export function getHighestSafeWindowContext(self = global.window.self) { + // If we reached the top level, return self + if (self === global.window.top) { + return self; + } + + const getOrigin = href => href.match(/(.*\/\/.*?)(\/|$)/)[1]; + + // If parent is the same origin, we can move up one context + // Reference: https://stackoverflow.com/a/21965342/1601953 + if (getOrigin(self.location.href) === getOrigin(self.document.referrer)) { + return getHighestSafeWindowContext(self.parent); + } + + // If a different origin, we consider the current level + // as the top reachable one + return self; +} diff --git a/front/odiparpack/app/components/Loading/index.js b/front/odiparpack/app/components/Loading/index.js new file mode 100644 index 0000000..41c92af --- /dev/null +++ b/front/odiparpack/app/components/Loading/index.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { PropTypes } from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { CircularProgress } from '@material-ui/core'; +const styles = { + circularProgress: { + position: 'fixed', + top: 'calc(50% - 30px)', + left: 'calc(50% - 30px)', + } +}; + +function Loading(props) { + return (<CircularProgress className={props.classes.circularProgress} size={60} color="secondary" />); +} + +Loading.propTypes = { + classes: PropTypes.object.isRequired, +}; +export default (withStyles(styles)(Loading)); diff --git a/front/odiparpack/app/components/Notification/Notification.js b/front/odiparpack/app/components/Notification/Notification.js new file mode 100644 index 0000000..7e73896 --- /dev/null +++ b/front/odiparpack/app/components/Notification/Notification.js @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import CloseIcon from '@material-ui/icons/Close'; + +import { Snackbar, IconButton } from '@material-ui/core'; + +const styles = theme => ({ + close: { + width: theme.spacing(4), + }, +}); + +class Notification extends React.Component { + handleClose = (event, reason) => { + if (reason === 'clickaway') { + return; + } + this.props.close('crudTableDemo'); + }; + + render() { + const { classes, message } = this.props; + return ( + <Snackbar + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + open={message !== ''} + autoHideDuration={3000} + onClose={() => this.handleClose()} + ContentProps={{ + 'aria-describedby': 'message-id', + }} + message={message} + action={[ + <IconButton + key="close" + aria-label="Close" + color="inherit" + className={classes.close} + onClick={() => this.handleClose()} + > + <CloseIcon /> + </IconButton>, + ]} + /> + ); + } +} + +Notification.propTypes = { + classes: PropTypes.object.isRequired, + close: PropTypes.func.isRequired, + message: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(Notification); diff --git a/front/odiparpack/app/components/Pagination/Pagination.js b/front/odiparpack/app/components/Pagination/Pagination.js new file mode 100644 index 0000000..2ac2e29 --- /dev/null +++ b/front/odiparpack/app/components/Pagination/Pagination.js @@ -0,0 +1,189 @@ +import React from 'react'; +import { createUltimatePagination, ITEM_TYPES } from 'react-ultimate-pagination'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import NavigationFirstPage from '@material-ui/icons/FirstPage'; +import NavigationLastPage from '@material-ui/icons/LastPage'; +import NavigationChevronLeft from '@material-ui/icons/ChevronLeft'; +import NavigationChevronRight from '@material-ui/icons/ChevronRight'; + +import { Button, Hidden, IconButton } from '@material-ui/core'; + +const flatButtonStyle = { + minWidth: 36 +}; + +const styles = { + paging: { + marginTop: 10, + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + } +}; + +const Page = ({ + value, + isActive, + onClick, + isDisabled +}) => ( + <Button + style={flatButtonStyle} + color={isActive ? 'primary' : 'default'} + onClick={onClick} + disabled={isDisabled} + > + {value.toString()} + </Button> +); + +Page.propTypes = { + value: PropTypes.number.isRequired, + isActive: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + isDisabled: PropTypes.bool.isRequired, +}; + +const Ellipsis = ({ onClick, isDisabled }) => ( + <Button + style={flatButtonStyle} + onClick={onClick} + disabled={isDisabled} + > + ... + </Button> +); + +Ellipsis.propTypes = { + onClick: PropTypes.func.isRequired, + isDisabled: PropTypes.bool.isRequired, +}; + +const FirstPageLink = ({ onClick, isDisabled }) => ( + <IconButton + style={flatButtonStyle} + onClick={onClick} + disabled={isDisabled} + > + <NavigationFirstPage /> + </IconButton> +); + + +FirstPageLink.propTypes = { + onClick: PropTypes.func.isRequired, + isDisabled: PropTypes.bool.isRequired, +}; + +const PreviousPageLink = ({ onClick, isDisabled }) => ( + <IconButton + style={flatButtonStyle} + onClick={onClick} + disabled={isDisabled} + > + <NavigationChevronLeft /> + </IconButton> +); + +PreviousPageLink.propTypes = { + onClick: PropTypes.func.isRequired, + isDisabled: PropTypes.bool.isRequired, +}; + +const NextPageLink = ({ onClick, isDisabled }) => ( + <IconButton + style={flatButtonStyle} + onClick={onClick} + disabled={isDisabled} + > + <NavigationChevronRight /> + </IconButton> +); + +NextPageLink.propTypes = { + onClick: PropTypes.func.isRequired, + isDisabled: PropTypes.bool.isRequired, +}; + +const LastPageLink = ({ onClick, isDisabled }) => ( + <IconButton + style={flatButtonStyle} + onClick={onClick} + disabled={isDisabled} + > + <NavigationLastPage /> + </IconButton> +); + +LastPageLink.propTypes = { + onClick: PropTypes.func.isRequired, + isDisabled: PropTypes.bool.isRequired, +}; + + +const itemTypeToComponent = { + [ITEM_TYPES.PAGE]: Page, + [ITEM_TYPES.ELLIPSIS]: Ellipsis, + [ITEM_TYPES.FIRST_PAGE_LINK]: FirstPageLink, + [ITEM_TYPES.PREVIOUS_PAGE_LINK]: PreviousPageLink, + [ITEM_TYPES.NEXT_PAGE_LINK]: NextPageLink, + [ITEM_TYPES.LAST_PAGE_LINK]: LastPageLink +}; + +const UltmPagination = createUltimatePagination({ itemTypeToComponent }); + +class Pagination extends React.Component { + constructor(props) { + super(); + this.state = { + totalPages: props.totpages + }; + } + + render() { + const hide = true; + const { totalPages } = this.state; + const { + classes, + curpage, + onChange, + onGoFirst, + onPrev, + onNext, + onGoLast, + ...rest + } = this.props; + return ( + <div className={classes.paging}> + <FirstPageLink isDisabled={curpage <= 1} onClick={onGoFirst} /> + <PreviousPageLink isDisabled={curpage <= 1} onClick={onPrev} /> + <Hidden xsDown> + <UltmPagination + currentPage={curpage} + totalPages={totalPages} + onChange={onChange} + hidePreviousAndNextPageLinks={hide} + hideFirstAndLastPageLinks={hide} + {...rest} + /> + </Hidden> + <NextPageLink isDisabled={curpage >= totalPages} onClick={onNext} /> + <LastPageLink isDisabled={curpage >= totalPages} onClick={onGoLast} /> + </div> + ); + } +} + +Pagination.propTypes = { + curpage: PropTypes.number.isRequired, + totpages: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, + onPrev: PropTypes.func.isRequired, + onNext: PropTypes.func.isRequired, + onGoFirst: PropTypes.func.isRequired, + onGoLast: PropTypes.func.isRequired, + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(Pagination); diff --git a/front/odiparpack/app/components/Panel/FloatingPanel.js b/front/odiparpack/app/components/Panel/FloatingPanel.js new file mode 100644 index 0000000..675166a --- /dev/null +++ b/front/odiparpack/app/components/Panel/FloatingPanel.js @@ -0,0 +1,90 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { isWidthDown } from '@material-ui/core/withWidth'; +import classNames from 'classnames'; +import CloseIcon from '@material-ui/icons/Close'; +import ExpandIcon from '@material-ui/icons/CallMade'; +import MinimizeIcon from '@material-ui/icons/CallReceived'; +import { withWidth, Tooltip, IconButton } from '@material-ui/core'; +import styles from './panel-jss'; + + +class FloatingPanel extends React.Component { + state = { + expanded: false + } + + toggleExpand() { + this.setState({ expanded: !this.state.expanded }); + } + + render() { + const { + classes, + openForm, + closeForm, + children, + branch, + title, + extraSize, + width + } = this.props; + const { expanded } = this.state; + return ( + <div> + <div className={ + classNames( + classes.formOverlay, + openForm && (isWidthDown('sm', width) || expanded) ? classes.showForm : classes.hideForm + )} + /> + <section className={ + classNames( + !openForm ? classes.hideForm : classes.showForm, + expanded ? classes.expanded : '', + classes.floatingForm, + classes.formTheme, + extraSize && classes.large + )} + > + <header> + { title } + <div className={classes.btnOpt}> + <Tooltip title={expanded ? 'Exit Full Screen' : 'Full Screen'}> + <IconButton className={classes.expandButton} onClick={() => this.toggleExpand()} aria-label="Expand"> + {expanded ? <MinimizeIcon /> : <ExpandIcon />} + </IconButton> + </Tooltip> + <Tooltip title="Close"> + <IconButton className={classes.closeButton} onClick={() => closeForm(branch)} aria-label="Close"> + <CloseIcon /> + </IconButton> + </Tooltip> + </div> + </header> + {children} + </section> + </div> + ); + } +} + +FloatingPanel.propTypes = { + classes: PropTypes.object.isRequired, + openForm: PropTypes.bool.isRequired, + closeForm: PropTypes.func.isRequired, + children: PropTypes.node.isRequired, + branch: PropTypes.string.isRequired, + width: PropTypes.string.isRequired, + title: PropTypes.string, + extraSize: PropTypes.bool, +}; + +FloatingPanel.defaultProps = { + title: 'Add New Item', + extraSize: false, +}; + +const FloatingPanelResponsive = withWidth()(FloatingPanel); +export default withStyles(styles)(FloatingPanelResponsive); diff --git a/front/odiparpack/app/components/Panel/panel-jss.js b/front/odiparpack/app/components/Panel/panel-jss.js new file mode 100644 index 0000000..d5d5e9c --- /dev/null +++ b/front/odiparpack/app/components/Panel/panel-jss.js @@ -0,0 +1,95 @@ +import { darken } from '@material-ui/core/styles/colorManipulator'; +const expand = { + bottom: 'auto', + right: 'auto', + left: '50%', + top: '50%', + transform: 'translateX(-50%) translateY(-50%)' +}; + +const styles = theme => ({ + formTheme: { + background: darken(theme.palette.primary.dark, 0.2), + boxShadow: theme.shadows[7] + }, + hideForm: { + display: 'none' + }, + showForm: { + display: 'block' + }, + btnOpt: {}, + expandButton: { + [theme.breakpoints.down('sm')]: { + display: 'none' + } + }, + floatingForm: { + position: 'fixed', + width: 500, + bottom: 10, + right: 10, + zIndex: 1300, + borderRadius: 3, + overflow: 'hidden', + [theme.breakpoints.down('sm')]: { + width: '95% !important', + ...expand + }, + '& header': { + color: theme.palette.common.white, + padding: '15px 20px', + '& $btnOpt': { + position: 'absolute', + right: 0, + top: 0, + '& > *': { + margin: '0 5px' + }, + '& $expandButton': { + transform: 'rotate(270deg)' + }, + '& svg': { + fill: theme.palette.common.white, + } + } + }, + }, + bodyForm: { + position: 'relative', + background: theme.palette.common.white, + padding: '15px 30px', + maxHeight: 900, + overflow: 'auto' + }, + buttonArea: { + background: theme.palette.grey[100], + position: 'relative', + bottom: 0, + left: 0, + width: '100%', + textAlign: 'right', + padding: '10px 30px', + '& button': { + marginRight: 5 + } + }, + expanded: { + ...expand + }, + formOverlay: { + position: 'fixed', + background: theme.palette.grey[900], + opacity: 0.7, + width: '100%', + height: '100%', + top: 0, + left: 0, + zIndex: 1300, + }, + large: { + width: 650 + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/PapperBlock/PapperBlock.js b/front/odiparpack/app/components/PapperBlock/PapperBlock.js new file mode 100644 index 0000000..6663ae6 --- /dev/null +++ b/front/odiparpack/app/components/PapperBlock/PapperBlock.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import { Paper, Typography } from '@material-ui/core'; +import styles from './papperStyle-jss'; + + +function PaperSheet(props) { + const { + classes, + title, + desc, + children, + whiteBg, + noMargin, + colorMode, + overflowX + } = props; + return ( + <div> + <Paper className={classNames(classes.root, noMargin && classes.noMargin, colorMode && classes.colorMode)} elevation={4}> + <Typography variant="h6" component="h2" className={classes.title}> + {title} + </Typography> + <Typography component="p" className={classes.description}> + {desc} + </Typography> + <section className={classNames(classes.content, whiteBg && classes.whiteBg, overflowX && classes.overflowX)}> + {children} + </section> + </Paper> + </div> + ); +} + +PaperSheet.propTypes = { + classes: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + desc: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + whiteBg: PropTypes.bool, + colorMode: PropTypes.bool, + noMargin: PropTypes.bool, + overflowX: PropTypes.bool, +}; + +PaperSheet.defaultProps = { + whiteBg: false, + noMargin: false, + colorMode: false, + overflowX: false +}; + +export default withStyles(styles)(PaperSheet); diff --git a/front/odiparpack/app/components/PapperBlock/papperStyle-jss.js b/front/odiparpack/app/components/PapperBlock/papperStyle-jss.js new file mode 100644 index 0000000..521e349 --- /dev/null +++ b/front/odiparpack/app/components/PapperBlock/papperStyle-jss.js @@ -0,0 +1,58 @@ +const styles = theme => ({ + root: theme.mixins.gutters({ + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(3), + marginTop: theme.spacing(3), + '&$noMargin': { + margin: 0 + }, + }), + title: { + marginBottom: theme.spacing(4), + paddingBottom: theme.spacing(2), + position: 'relative', + textTransform: 'capitalize', + fontSize: 28, + '&:after': { + content: '""', + display: 'block', + position: 'absolute', + bottom: 0, + left: 0, + width: 40, + borderBottom: `4px solid ${theme.palette.primary.main}` + } + }, + description: { + maxWidth: 960, + fontSize: 16, + }, + content: { + marginTop: theme.spacing(2), + padding: theme.spacing(1), + backgroundColor: theme.palette.background.default, + }, + whiteBg: { + backgroundColor: 'transparent', + margin: 0, + }, + noMargin: {}, + colorMode: { + backgroundColor: theme.palette.secondary.main, + '& $title': { + color: theme.palette.grey[100], + '&:after': { + borderBottom: `5px solid ${theme.palette.primary.light}` + } + }, + '& $description': { + color: theme.palette.grey[100], + } + }, + overflowX: { + width: '100%', + overflowX: 'auto', + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Profile/About.js b/front/odiparpack/app/components/Profile/About.js new file mode 100644 index 0000000..9872a47 --- /dev/null +++ b/front/odiparpack/app/components/Profile/About.js @@ -0,0 +1,243 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import LocalPhone from '@material-ui/icons/LocalPhone'; +import DateRange from '@material-ui/icons/DateRange'; +import LocationOn from '@material-ui/icons/LocationOn'; +import InfoIcon from '@material-ui/icons/Info'; +import Check from '@material-ui/icons/Check'; +import AcUnit from '@material-ui/icons/AcUnit'; +import Adb from '@material-ui/icons/Adb'; +import AllInclusive from '@material-ui/icons/AllInclusive'; +import AssistantPhoto from '@material-ui/icons/AssistantPhoto'; +import imgData from 'ba-api/imgData'; +import Type from 'ba-styles/Typography.scss'; +import { + Grid, + Paper, + Typography, + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, + Button, + LinearProgress, + Divider, + Chip, + GridList, + GridListTile, + GridListTileBar, + IconButton, +} from '@material-ui/core'; +import Timeline from '../SocialMedia/Timeline'; +import PapperBlock from '../PapperBlock/PapperBlock'; +import styles from './profile-jss'; + + +class About extends React.Component { + render() { + const { classes, data } = this.props; + return ( + <Grid + container + alignItems="flex-start" + justify="flex-start" + direction="row" + spacing={3} + > + <Grid item md={7} xs={12}> + <div> + <Timeline dataTimeline={data} /> + </div> + </Grid> + <Grid item md={5} xs={12}> + {/* Profile Progress */} + <div className={classes.progressRoot}> + <Paper className={classes.styledPaper} elevation={4}> + <Typography className={classes.title} variant="h5" component="h3"> + <span className={Type.light}>Profile Strength: </span> + <span className={Type.bold}>Intermediate</span> + </Typography> + <Grid container justify="center"> + <Chip + avatar={( + <Avatar> + <Check /> + </Avatar> + )} + label="60% Progress" + className={classes.chip} + color="primary" + /> + </Grid> + <LinearProgress variant="determinate" className={classes.progress} value={60} /> + </Paper> + </div> + {/* ----------------------------------------------------------------------*/} + {/* About Me */} + <PapperBlock title="About Me" whiteBg noMargin desc="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum."> + <Divider className={classes.divider} /> + <List dense className={classes.profileList}> + <ListItem> + <ListItemAvatar> + <Avatar> + <DateRange /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Born" secondary="Jan 9, 1994" /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <LocalPhone /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Phone" secondary="(+62)8765432190" /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <LocationOn /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Address" secondary="Chicendo Street no.105 Block A/5A - Barcelona, Spain" /> + </ListItem> + </List> + </PapperBlock> + {/* ----------------------------------------------------------------------*/} + {/* My Albums */} + <PapperBlock title="My Albums (6)" whiteBg desc=""> + <div className={classes.albumRoot}> + <GridList cellHeight={180} className={classes.gridList}> + { + imgData.map((tile, index) => { + if (index >= 4) { + return false; + } + return ( + <GridListTile key={index.toString()}> + <img src={tile.img} className={classes.img} alt={tile.title} /> + <GridListTileBar + title={tile.title} + subtitle={( + <span> +by: + {tile.author} + </span> + )} + actionIcon={( + <IconButton className={classes.icon}> + <InfoIcon /> + </IconButton> + )} + /> + </GridListTile> + ); + }) + } + </GridList> + </div> + <Divider className={classes.divider} /> + <Grid container justify="center"> + <Button color="secondary" className={classes.button}> + See All + </Button> + </Grid> + </PapperBlock> + {/* ----------------------------------------------------------------------*/} + {/* My Connection Me */} + <PapperBlock title="My Connection" whiteBg desc=""> + <List dense className={classes.profileList}> + <ListItem button> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.orangeAvatar)}>H</Avatar> + </ListItemAvatar> + <ListItemText primary="Harry Wells" secondary="2 Mutual Connection" /> + </ListItem> + <ListItem button> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.purpleAvatar)}>J</Avatar> + </ListItemAvatar> + <ListItemText primary="John DOe" secondary="8 Mutual Connection" /> + </ListItem> + <ListItem button> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.pinkAvatar)}>V</Avatar> + </ListItemAvatar> + <ListItemText primary="Victor Wanggai" secondary="12 Mutual Connection" /> + </ListItem> + <ListItem button> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.greenAvatar)}>H</Avatar> + </ListItemAvatar> + <ListItemText primary="Baron Phoenix" secondary="10 Mutual Connection" /> + </ListItem> + </List> + <Divider className={classes.divider} /> + <Grid container justify="center"> + <Button color="secondary" className={classes.button}> + See All + </Button> + </Grid> + </PapperBlock> + {/* ----------------------------------------------------------------------*/} + {/* My Interests */} + <PapperBlock title="My Interests" whiteBg desc=""> + <Grid container className={classes.colList}> + <Grid item md={6}> + <ListItem> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.purpleAvatar)}> + <AcUnit /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Snow" secondary="100 Connected" /> + </ListItem> + </Grid> + <Grid item md={6}> + <ListItem> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.greenAvatar)}> + <Adb /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Android" secondary="120 Connected" /> + </ListItem> + </Grid> + <Grid item md={6}> + <ListItem> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.pinkAvatar)}> + <AllInclusive /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="All Inclusive" secondary="999+ Connected" /> + </ListItem> + </Grid> + <Grid item md={6}> + <ListItem> + <ListItemAvatar> + <Avatar className={classNames(classes.avatar, classes.orangeAvatar)}> + <AssistantPhoto /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="My Country" secondary="99+ Connected" /> + </ListItem> + </Grid> + </Grid> + </PapperBlock> + {/* ----------------------------------------------------------------------*/} + </Grid> + </Grid> + ); + } +} + +About.propTypes = { + classes: PropTypes.object.isRequired, + data: PropTypes.object.isRequired +}; + +export default withStyles(styles)(About); diff --git a/front/odiparpack/app/components/Profile/Albums.js b/front/odiparpack/app/components/Profile/Albums.js new file mode 100644 index 0000000..dbb6d19 --- /dev/null +++ b/front/odiparpack/app/components/Profile/Albums.js @@ -0,0 +1,152 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import imgData from 'ba-api/imgData'; +import { Grid, GridList, GridListTile, ButtonBase, Typography } from '@material-ui/core'; +import styles from './profile-jss'; + + +function Albums(props) { + const { classes } = props; + + return ( + <div className={classes.root}> + <Grid + container + direction="row" + spacing={3} + > + <Grid item md={6} sm={12} xs={12}> + <ButtonBase + focusRipple + className={classes.image} + focusVisibleClassName={classes.focusVisible} + > + <GridList cellHeight={160} className={classes.gridList} cols={3}> + {imgData.map((tile, index) => { + if (index > 6) { + return false; + } + return ( + <GridListTile key={index.toString()} cols={tile.cols || 1}> + <img src={tile.img} className={classes.img} alt={tile.title} /> + </GridListTile> + ); + })} + </GridList> + <span className={classes.imageBackdrop} /> + <span className={classes.imageButton}> + <Typography + component="span" + variant="subtitle1" + color="inherit" + className={classes.imageTitle} + > + Album Number One + <span className={classes.imageMarked} /> + </Typography> + </span> + </ButtonBase> + <ButtonBase + focusRipple + className={classes.image} + focusVisibleClassName={classes.focusVisible} + > + <GridList cellHeight={160} className={classes.gridListAlbum} cols={3}> + {imgData.map((tile, index) => { + if (index > 2 && index < 9) { + return false; + } + return ( + <GridListTile key={index.toString()} cols={tile.cols || 1}> + <img src={tile.img} className={classes.img} alt={tile.title} /> + </GridListTile> + ); + })} + </GridList> + <span className={classes.imageBackdrop} /> + <span className={classes.imageButton}> + <Typography + component="span" + variant="subtitle1" + color="inherit" + className={classes.imageTitle} + > + Album Number Three + <span className={classes.imageMarked} /> + </Typography> + </span> + </ButtonBase> + </Grid> + <Grid item md={6} sm={12} xs={12}> + <ButtonBase + focusRipple + className={classes.image} + focusVisibleClassName={classes.focusVisible} + > + <GridList cellHeight={160} className={classes.gridList} cols={3}> + {imgData.map((tile, index) => { + if (index > 4 && index < 10) { + return false; + } + return ( + <GridListTile key={index.toString()} cols={tile.cols || 1}> + <img src={tile.img} className={classes.img} alt={tile.title} /> + </GridListTile> + ); + })} + </GridList> + <span className={classes.imageBackdrop} /> + <span className={classes.imageButton}> + <Typography + component="span" + variant="subtitle1" + color="inherit" + className={classes.imageTitle} + > + Album Number Two + <span className={classes.imageMarked} /> + </Typography> + </span> + </ButtonBase> + <ButtonBase + focusRipple + className={classes.image} + focusVisibleClassName={classes.focusVisible} + > + <GridList cellHeight={160} className={classes.gridList} cols={3}> + {imgData.map((tile, index) => { + if (index % 2) { + return false; + } + return ( + <GridListTile key={index.toString()} cols={tile.cols || 1}> + <img src={tile.img} className={classes.img} alt={tile.title} /> + </GridListTile> + ); + })} + </GridList> + <span className={classes.imageBackdrop} /> + <span className={classes.imageButton}> + <Typography + component="span" + variant="subtitle1" + color="inherit" + className={classes.imageTitle} + > + Album Number Four + <span className={classes.imageMarked} /> + </Typography> + </span> + </ButtonBase> + </Grid> + </Grid> + </div> + ); +} + +Albums.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(Albums); diff --git a/front/odiparpack/app/components/Profile/Connection.js b/front/odiparpack/app/components/Profile/Connection.js new file mode 100644 index 0000000..beef128 --- /dev/null +++ b/front/odiparpack/app/components/Profile/Connection.js @@ -0,0 +1,45 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import datas from 'ba-api/connectionData'; +import { Grid } from '@material-ui/core'; +import ProfileCard from '../CardPaper/ProfileCard'; +import styles from './profile-jss'; + +class Connection extends React.Component { + render() { + const { classes } = this.props; + return ( + <Grid + container + alignItems="flex-start" + justify="space-between" + direction="row" + spacing={2} + className={classes.rootx} + > + { + datas.map((data, index) => ( + <Grid item md={4} sm={6} xs={12} key={index.toString()}> + <ProfileCard + cover={data.cover} + avatar={data.avatar} + name={data.name} + title={data.title} + connection={data.connection} + isVerified={data.verified} + btnText="See Profile" + /> + </Grid> + )) + } + </Grid> + ); + } +} + +Connection.propTypes = { + classes: PropTypes.object.isRequired +}; + +export default withStyles(styles)(Connection); diff --git a/front/odiparpack/app/components/Profile/Favorites.js b/front/odiparpack/app/components/Profile/Favorites.js new file mode 100644 index 0000000..ac6b506 --- /dev/null +++ b/front/odiparpack/app/components/Profile/Favorites.js @@ -0,0 +1,130 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import imgApi from 'ba-api/images'; +import avatarApi from 'ba-api/avatars'; +import { Typography, Grid, Divider } from '@material-ui/core'; +import GeneralCard from '../CardPaper/GeneralCard'; +import PostCard from '../CardPaper/PostCard'; +import Quote from '../Quote/Quote'; + + +const styles = theme => ({ + divider: { + margin: `${theme.spacing(3)}px 0`, + }, +}); + +class Favorites extends React.Component { + render() { + const { classes } = this.props; + const bull = <span className={classes.bullet}>•</span>; + return ( + <Grid + container + justify="center" + direction="row" + spacing={3} + > + <Grid item md={6}> + <PostCard + liked={1} + shared={20} + commented={15} + date="Sept, 25 2018" + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum." + image={imgApi[5]} + avatar={avatarApi[6]} + name="John Doe" + /> + <Divider className={classes.divider} /> + <GeneralCard liked={1} shared={20} commented={15}> + <Typography className={classes.title} color="textSecondary"> + Word of the Day + </Typography> + <Typography variant="h5" component="h2"> + be + {bull} +nev + {bull} +o + {bull} +lent + </Typography> + <Typography className={classes.pos} color="textSecondary"> + adjective + </Typography> + <Typography component="p"> + well meaning and kindly. + <br /> + {'"a benevolent smile"'} + </Typography> + </GeneralCard> + <Divider className={classes.divider} /> + <PostCard + liked={1} + shared={20} + commented={15} + date="Sept, 25 2018" + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum." + image={imgApi[16]} + avatar={avatarApi[6]} + name="John Doe" + /> + <Divider className={classes.divider} /> + <PostCard + liked={90} + shared={10} + commented={22} + date="Sept, 15 2018" + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum." + avatar={avatarApi[5]} + name="Jane Doe" + /> + </Grid> + <Grid item md={6}> + <PostCard + liked={90} + shared={10} + commented={22} + date="Sept, 15 2018" + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum." + avatar={avatarApi[4]} + name="Jane Doe" + /> + <Divider className={classes.divider} /> + <PostCard + liked={1} + shared={20} + commented={15} + date="Sept, 25 2018" + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum." + image={imgApi[20]} + avatar={avatarApi[6]} + name="John Doe" + /> + <Divider className={classes.divider} /> + <GeneralCard liked={1} shared={20} commented={15}> + <Quote align="left" content="Imagine all the people living life in peace. You may say I'm a dreamer, but I'm not the only one. I hope someday you'll join us, and the world will be as one." footnote="John Lennon" /> + </GeneralCard> + <Divider className={classes.divider} /> + <PostCard + liked={90} + shared={10} + commented={22} + date="Sept, 15 2018" + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum." + avatar={avatarApi[1]} + name="Jane Doe" + /> + </Grid> + </Grid> + ); + } +} + +Favorites.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(Favorites); diff --git a/front/odiparpack/app/components/Profile/profile-jss.js b/front/odiparpack/app/components/Profile/profile-jss.js new file mode 100644 index 0000000..32c3308 --- /dev/null +++ b/front/odiparpack/app/components/Profile/profile-jss.js @@ -0,0 +1,155 @@ +import { deepOrange, deepPurple, pink, green } from '@material-ui/core/colors'; +const styles = theme => ({ + profileList: { + padding: 0, + '& li': { + paddingLeft: 0 + } + }, + avatar: { + margin: 10, + }, + orangeAvatar: { + backgroundColor: deepOrange[500], + }, + purpleAvatar: { + backgroundColor: deepPurple[500], + }, + pinkAvatar: { + backgroundColor: pink[500], + }, + greenAvatar: { + backgroundColor: green[500], + }, + divider: { + margin: `${theme.spacing(3)}px 0`, + }, + albumRoot: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-around', + overflow: 'hidden', + backgroundColor: theme.palette.background.paper, + }, + gridList: { + width: 500, + height: 'auto', + }, + icon: { + color: 'rgba(255, 255, 255, 0.54)', + }, + img: { + maxWidth: 'none' + }, + root: theme.mixins.gutters({ + paddingTop: 16, + paddingBottom: 16, + marginTop: theme.spacing(3), + }), + progressRoot: { + marginBottom: 30, + }, + styledPaper: { + backgroundColor: theme.palette.secondary.main, + padding: 20, + '& $title, & $subtitle': { + color: theme.palette.common.white + } + }, + progress: { + marginTop: 20, + background: theme.palette.secondary.dark, + '& div': { + background: theme.palette.primary.light, + } + }, + chip: { + marginTop: 20, + background: theme.palette.primary.light, + '& div': { + background: green[500], + color: theme.palette.common.white + } + }, + colList: { + '& li': { + padding: '10px 0' + }, + '& $avatar': { + margin: 0 + } + }, + title: {}, + subtitle: {}, + rootAlbum: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-around', + overflow: 'hidden', + }, + image: { + position: 'relative', + height: 'auto', + boxShadow: theme.shadows[6], + borderRadius: 2, + overflow: 'hidden', + marginBottom: 30, + width: '100% !important', // Overrides inline-style + '&:hover, &$focusVisible': { + zIndex: 1, + '& $imageBackdrop': { + opacity: 0.6, + }, + '& $imageMarked': { + opacity: 0, + }, + '& $imageTitle': { + border: '4px solid currentColor', + }, + }, + }, + imageButton: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.palette.common.white, + }, + imageBackdrop: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundColor: theme.palette.common.black, + opacity: 0.4, + transition: theme.transitions.create('opacity'), + }, + imageTitle: { + position: 'relative', + padding: `${theme.spacing(2)}px ${theme.spacing(4)}px ${theme.spacing(1) + 6}px`, + }, + imageMarked: { + height: 3, + width: 18, + backgroundColor: theme.palette.common.white, + position: 'absolute', + bottom: -2, + left: 'calc(50% - 9px)', + transition: theme.transitions.create('opacity'), + }, + focusVisible: {}, + gridListAlbum: { + height: 'auto', + background: theme.palette.common.black + }, + subheader: { + width: '100%', + }, +}); + +export default styles; diff --git a/front/odiparpack/app/components/Quote/Quote.js b/front/odiparpack/app/components/Quote/Quote.js new file mode 100644 index 0000000..21665d5 --- /dev/null +++ b/front/odiparpack/app/components/Quote/Quote.js @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { blueGrey } from '@material-ui/core/colors'; +import { Typography } from '@material-ui/core'; + +const styles = ({ + quoteWrap: { + padding: '0 25', + margin: 10, + position: 'relative', + '&:before': { + color: blueGrey[100], + fontSize: '4em', + lineHeight: '.1em', + marginRight: '.25em', + verticalAlign: '-.4em' + } + }, + quoteLeft: { + extend: 'quoteWrap', + textAlign: 'left', + borderLeft: '5px solid' + blueGrey[50], + paddingLeft: 25, + '&:before': { + content: 'open-quote', + } + }, + quoteRight: { + extend: 'quoteWrap', + textAlign: 'right', + borderRight: '5px solid' + blueGrey[50], + paddingRight: 25, + '&:before': { + content: 'close-quote', + } + }, + quoteBody: { + minHeight: 100, + marginBottom: 20 + } +}); + + +class Quote extends React.Component { + render() { + const { + align, + content, + footnote, + classes + } = this.props; + return ( + <div + className={ + classNames( + classes.quoteWrap, + align === 'right' ? classes.quoteRight : classes.quoteLeft + ) + } + > + <Typography variant="subtitle1" className={classes.quoteBody} gutterBottom> + {content} + </Typography> + <Typography variant="caption"> + {footnote} + </Typography> + </div> + ); + } +} + +Quote.propTypes = { + align: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + footnote: PropTypes.string.isRequired, + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(Quote); diff --git a/front/odiparpack/app/components/Rating/Rating.js b/front/odiparpack/app/components/Rating/Rating.js new file mode 100644 index 0000000..6f65d06 --- /dev/null +++ b/front/odiparpack/app/components/Rating/Rating.js @@ -0,0 +1,144 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ToggleStar from '@material-ui/icons/Star'; +import ToggleStarBorder from '@material-ui/icons/StarBorder'; +import { orange, grey } from '@material-ui/core/colors'; + +import { IconButton } from '@material-ui/core'; + +const styles = { + disabled: { + pointerEvents: 'none' + } +}; + +class Rating extends Component { + constructor(props) { + super(props); + this.state = { + hoverValue: props.value + }; + } + + renderIcon(i) { + const filled = i <= this.props.value; + const hovered = i <= this.state.hoverValue; + + if ((hovered && !filled) || (!hovered && filled)) { + return this.props.iconHoveredRenderer ? this.props.iconHoveredRenderer({ + ...this.props, + index: i + }) : this.props.iconHovered; + } if (filled) { + return this.props.iconFilledRenderer ? this.props.iconFilledRenderer({ + ...this.props, + index: i + }) : this.props.iconFilled; + } + return this.props.iconNormalRenderer ? this.props.iconNormalRenderer({ + ...this.props, + index: i + }) : this.props.iconNormal; + } + + render() { + const { + disabled, + iconFilled, + iconHovered, + iconNormal, + tooltip, + tooltipRenderer, + tooltipPosition, + tooltipStyles, + iconFilledRenderer, + iconHoveredRenderer, + iconNormalRenderer, + itemStyle, + itemClassName, + itemIconStyle, + max, + onChange, + readOnly, + style, + value, + ...other + } = this.props; + + const rating = []; + + for (let i = 1; i <= max; i += 1) { + rating.push( + <IconButton + key={i} + className={itemClassName} + disabled={disabled} + onMouseEnter={() => this.setState({ hoverValue: i })} + onMouseLeave={() => this.setState({ hoverValue: value })} + onClick={() => { + if (!readOnly && onChange) { + onChange(i); + } + }} + > + {this.renderIcon(i)} + </IconButton> + ); + } + + return ( + <div + style={this.props.disabled || this.props.readOnly ? { ...styles.disabled, ...this.props.style } : this.props.style} + {...other} + > + {rating} + </div> + ); + } +} + +Rating.propTypes = { + disabled: PropTypes.bool, + iconFilled: PropTypes.node, + iconHovered: PropTypes.node, + iconNormal: PropTypes.node, + tooltip: PropTypes.node, + tooltipRenderer: PropTypes.func, + tooltipPosition: PropTypes.string, + tooltipStyles: PropTypes.object, + iconFilledRenderer: PropTypes.func, + iconHoveredRenderer: PropTypes.func, + iconNormalRenderer: PropTypes.func, + itemStyle: PropTypes.object, + itemClassName: PropTypes.object, + itemIconStyle: PropTypes.object, + max: PropTypes.number, + onChange: PropTypes.func, + readOnly: PropTypes.bool, + style: PropTypes.object, + value: PropTypes.number +}; + +Rating.defaultProps = { + disabled: false, + iconFilled: <ToggleStar style={{ color: orange[500] }} />, + iconHovered: <ToggleStarBorder style={{ color: orange[500] }} />, + iconNormal: <ToggleStarBorder style={{ color: grey[300] }} />, + tooltipPosition: 'bottom-center', + max: 5, + readOnly: false, + value: 0, + tooltip: null, + tooltipRenderer: () => {}, + tooltipStyles: null, + iconFilledRenderer: undefined, + iconHoveredRenderer: undefined, + iconNormalRenderer: undefined, + itemStyle: undefined, + itemClassName: undefined, + itemIconStyle: undefined, + onChange: () => {}, + style: null, +}; + +export default Rating; diff --git a/front/odiparpack/app/components/Search/SearchProduct.js b/front/odiparpack/app/components/Search/SearchProduct.js new file mode 100644 index 0000000..b465808 --- /dev/null +++ b/front/odiparpack/app/components/Search/SearchProduct.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; +import SearchIcon from '@material-ui/icons/Search'; +import { AppBar, Toolbar, IconButton, Badge } from '@material-ui/core'; +import Cart from '../Cart/Cart'; +import styles from './search-jss'; + + +class SearchProduct extends React.Component { + state = { + anchorEl: null, + }; + + handleClick = event => { + this.setState({ anchorEl: event.currentTarget }); + }; + + handleClose = () => { + this.setState({ anchorEl: null }); + }; + + render() { + const { anchorEl } = this.state; + const { + classes, + dataCart, + removeItem, + checkout, + totalItems, + totalPrice, + search + } = this.props; + return ( + <div className={classes.root}> + <AppBar position="static" color="inherit"> + <Toolbar> + <div className={classes.flex}> + <div className={classes.wrapper}> + <div className={classes.search}> + <SearchIcon /> + </div> + <input className={classes.input} placeholder="Search Product" onChange={(event) => search(event)} /> + </div> + </div> + <div> + <IconButton + color="inherit" + aria-owns={anchorEl ? 'simple-menu' : null} + aria-haspopup="true" + onClick={this.handleClick} + > + <Badge badgeContent={totalItems} color="secondary"> + <ShoppingCartIcon /> + </Badge> + </IconButton> + <Cart + anchorEl={anchorEl} + dataCart={dataCart} + close={this.handleClose} + removeItem={removeItem} + checkout={checkout} + totalPrice={totalPrice} + /> + </div> + </Toolbar> + </AppBar> + </div> + ); + } +} + +SearchProduct.propTypes = { + classes: PropTypes.object.isRequired, + dataCart: PropTypes.object.isRequired, + removeItem: PropTypes.func.isRequired, + search: PropTypes.func.isRequired, + checkout: PropTypes.func.isRequired, + totalItems: PropTypes.number.isRequired, + totalPrice: PropTypes.number.isRequired, +}; + +export default withStyles(styles)(SearchProduct); diff --git a/front/odiparpack/app/components/Search/search-jss.js b/front/odiparpack/app/components/Search/search-jss.js new file mode 100644 index 0000000..5ba6ee0 --- /dev/null +++ b/front/odiparpack/app/components/Search/search-jss.js @@ -0,0 +1,48 @@ +const styles = theme => ({ + root: { + flexGrow: 1, + marginTop: 20, + marginBottom: 40 + }, + flex: { + flex: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, + wrapper: { + fontFamily: theme.typography.fontFamily, + position: 'relative', + marginRight: theme.spacing(2), + marginLeft: theme.spacing(1), + borderRadius: 2, + display: 'block', + }, + search: { + width: 'auto', + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + input: { + font: 'inherit', + padding: `${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(1)}px ${theme.spacing(4)}px`, + border: 0, + display: 'block', + verticalAlign: 'middle', + whiteSpace: 'normal', + background: 'none', + margin: 0, // Reset for Safari + color: 'inherit', + width: '100%', + '&:focus': { + outline: 0, + }, + }, +}); + +export default styles; diff --git a/front/odiparpack/app/components/Sidebar/MainMenu.js b/front/odiparpack/app/components/Sidebar/MainMenu.js new file mode 100644 index 0000000..edeb420 --- /dev/null +++ b/front/odiparpack/app/components/Sidebar/MainMenu.js @@ -0,0 +1,130 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { NavLink } from 'react-router-dom'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +// Menu Object +import MenuContent from 'ba-api/menu'; +import { List, ListItem, ListItemIcon, ListItemText, Collapse, Icon } from '@material-ui/core'; +import styles from './sidebar-jss'; + +function sortByKey(array, key) { + return array.sort((a, b) => { + const x = a[key]; const y = b[key]; + return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + }); +} + +const LinkBtn = React.forwardRef(function LinkBtn(props, ref) { // eslint-disable-line + return <NavLink to={props.to} {...props} innerRef={ref} />; // eslint-disable-line +}); + +function MainMenu(props) { + const { + classes, + toggleDrawerOpen, + loadTransition, + openSubMenu, + open, + } = props; + + const handleClick = () => { + toggleDrawerOpen(); + loadTransition(false); + }; + + const getMenus = menuArray => menuArray.map((item, index) => { + if (item.child) { + return ( + <div key={index.toString()}> + <ListItem + button + className={classNames(classes.head, open.indexOf(item.key) > -1 ? classes.opened : '')} + onClick={() => openSubMenu(item.key, item.keyParent)} + > + {item.icon + && ( + <ListItemIcon className={classes.iconWrapper}> + <Icon className={classes.icon}>{item.icon}</Icon> + </ListItemIcon> + ) + } + <ListItemText classes={{ primary: classes.primary }} variant="inset" primary={item.name} /> + { open.indexOf(item.key) > -1 ? <ExpandLess /> : <ExpandMore /> } + </ListItem> + <Collapse + component="li" + className={classNames( + classes.nolist, + (item.keyParent ? classes.child : ''), + )} + in={open.indexOf(item.key) > -1} + timeout="auto" + unmountOnExit + > + <List className={classes.dense} dense> + { getMenus(sortByKey(item.child, 'key')) } + </List> + </Collapse> + </div> + ); + } + return ( + <ListItem + key={index.toString()} + button + exact + className={classes.nested} + activeClassName={classes.active} + component={LinkBtn} + to={item.link} + onClick={handleClick} + > + {item.icon + && ( + <ListItemIcon> + <Icon className={classes.icon}>{item.icon}</Icon> + </ListItemIcon> + ) + } + <ListItemText classes={{ primary: classes.primary }} inset primary={item.name} /> + </ListItem> + ); + }); + return ( + <div> + {getMenus(MenuContent)} + </div> + ); +} + +MainMenu.propTypes = { + classes: PropTypes.object.isRequired, + open: PropTypes.object.isRequired, + openSubMenu: PropTypes.func.isRequired, + toggleDrawerOpen: PropTypes.func.isRequired, + loadTransition: PropTypes.func.isRequired, +}; + +const openAction = (key, keyParent) => ({ type: 'OPEN_SUBMENU', key, keyParent }); +const reducer = 'ui'; + +const mapStateToProps = state => ({ + force: state, // force active class for sidebar menu + open: state.getIn([reducer, 'subMenuOpen']) +}); + +const mapDispatchToProps = dispatch => ({ + openSubMenu: bindActionCreators(openAction, dispatch) +}); + +const MainMenuMapped = connect( + mapStateToProps, + mapDispatchToProps +)(MainMenu); + +export default withStyles(styles)(MainMenuMapped); diff --git a/front/odiparpack/app/components/Sidebar/OtherMenu.js b/front/odiparpack/app/components/Sidebar/OtherMenu.js new file mode 100644 index 0000000..ea2f597 --- /dev/null +++ b/front/odiparpack/app/components/Sidebar/OtherMenu.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { NavLink } from 'react-router-dom'; +import OtherMenuContent from 'ba-api/otherMenu'; +import { ListItem, ListItemText } from '@material-ui/core'; +import styles from './sidebar-jss'; + +const LinkBtn = React.forwardRef(function LinkBtn(props, ref) { // eslint-disable-line + return <NavLink to={props.to} {...props} innerRef={ref} />; // eslint-disable-line +}); + +function OtherMenu(props) { + const { toggleDrawerOpen, classes } = props; + const getOtherMenu = menuArray => menuArray.map((item, index) => { + const keyIndex = index.toString(); + return ( + <div key={keyIndex}> + <ListItem + button + component={LinkBtn} + to={item.link} + activeClassName={classes.active} + onClick={toggleDrawerOpen} + > + <ListItemText secondary={item.name} /> + </ListItem> + </div> + ); + }); + + return ( + <div> + {getOtherMenu(OtherMenuContent)} + </div> + ); +} + +OtherMenu.propTypes = { + classes: PropTypes.object.isRequired, + toggleDrawerOpen: PropTypes.func.isRequired, +}; + +export default withStyles(styles)(OtherMenu); diff --git a/front/odiparpack/app/components/Sidebar/Sidebar.js b/front/odiparpack/app/components/Sidebar/Sidebar.js new file mode 100644 index 0000000..01de4ec --- /dev/null +++ b/front/odiparpack/app/components/Sidebar/Sidebar.js @@ -0,0 +1,122 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import brand from 'ba-api/brand'; +import dummy from 'ba-api/dummyContents'; +import logo from 'ba-images/logo.svg'; +import { Hidden, Drawer, SwipeableDrawer, List, Divider, Avatar } from '@material-ui/core'; +import MainMenu from './MainMenu'; +import OtherMenu from './OtherMenu'; +import styles from './sidebar-jss'; + +const MenuContent = props => { + const { + classes, + turnDarker, + drawerPaper, + toggleDrawerOpen, + loadTransition + } = props; + return ( + <div className={classNames(classes.drawerInner, !drawerPaper ? classes.drawerPaperClose : '')}> + <div className={classes.drawerHeader}> + <div className={classNames(classes.brand, classes.brandBar, turnDarker && classes.darker)}> + <img src={logo} alt={brand.name} /> + <h3>{brand.name}</h3> + </div> + <div className={classNames(classes.profile, classes.user)}> + <Avatar + alt={dummy.user.name} + src={dummy.user.avatar} + className={classNames(classes.avatar, classes.bigAvatar)} + /> + <div> + <h4>{dummy.user.name}</h4> + <span>{dummy.user.title}</span> + </div> + </div> + </div> + <div className={classes.menuContainer}> + <MainMenu loadTransition={loadTransition} toggleDrawerOpen={toggleDrawerOpen} /> + <Divider className={classes.divider} /> + <List> + <OtherMenu toggleDrawerOpen={toggleDrawerOpen} /> + </List> + </div> + </div> + ); +}; + +MenuContent.propTypes = { + classes: PropTypes.object.isRequired, + drawerPaper: PropTypes.bool.isRequired, + turnDarker: PropTypes.bool, + toggleDrawerOpen: PropTypes.func, + loadTransition: PropTypes.func, +}; + +MenuContent.defaultProps = { + turnDarker: false +}; + +MenuContent.defaultProps = { + toggleDrawerOpen: () => {}, + loadTransition: () => {}, +}; + +const MenuContentStyle = withStyles(styles)(MenuContent); + +function Sidebar(props) { + const { + classes, + open, + toggleDrawerOpen, + loadTransition, + turnDarker + } = props; + + return ( + <Fragment> + <Hidden lgUp> + <SwipeableDrawer + onClose={toggleDrawerOpen} + onOpen={toggleDrawerOpen} + open={!open} + anchor="left" + > + <div className={classes.swipeDrawerPaper}> + <MenuContentStyle drawerPaper toggleDrawerOpen={toggleDrawerOpen} loadTransition={loadTransition} /> + </div> + </SwipeableDrawer> + </Hidden> + <Hidden mdDown> + <Drawer + variant="permanent" + onClose={toggleDrawerOpen} + classes={{ + paper: classNames(classes.drawer, classes.drawerPaper, !open ? classes.drawerPaperClose : ''), + }} + open={open} + anchor="left" + > + <MenuContentStyle + drawerPaper={open} + turnDarker={turnDarker} + loadTransition={loadTransition} + /> + </Drawer> + </Hidden> + </Fragment> + ); +} + +Sidebar.propTypes = { + classes: PropTypes.object.isRequired, + toggleDrawerOpen: PropTypes.func.isRequired, + loadTransition: PropTypes.func.isRequired, + turnDarker: PropTypes.bool.isRequired, + open: PropTypes.bool.isRequired, +}; + +export default withStyles(styles)(Sidebar); diff --git a/front/odiparpack/app/components/Sidebar/sidebar-jss.js b/front/odiparpack/app/components/Sidebar/sidebar-jss.js new file mode 100644 index 0000000..e9bf4f6 --- /dev/null +++ b/front/odiparpack/app/components/Sidebar/sidebar-jss.js @@ -0,0 +1,205 @@ +const drawerWidth = 240; +const styles = theme => ({ + user: { + justifyContent: 'center' + }, + drawerPaper: { + position: 'relative', + height: '100%', + overflow: 'hidden', + backgroundColor: theme.palette.background.default, + border: 'none', + width: drawerWidth, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + swipeDrawerPaper: { + width: drawerWidth, + }, + opened: { + background: theme.palette.grey[200], + '& $primary, & $icon': { + color: theme.palette.secondary.dark, + }, + }, + drawerInner: { + height: '100%', + position: 'fixed', + width: drawerWidth, + }, + drawerPaperClose: { + width: 66, + position: 'fixed', + overflowX: 'hidden', + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + '& $user': { + justifyContent: 'flex-start' + }, + '& $bigAvatar': { + width: 40, + height: 40, + }, + '& li ul': { + display: 'none' + }, + '&:hover': { + width: drawerWidth, + boxShadow: theme.shadows[6], + '& li ul': { + display: 'block' + } + }, + '& $menuContainer': { + paddingLeft: theme.spacing(1.5), + paddingRight: theme.spacing(1.5), + width: drawerWidth, + }, + '& $drawerInner': { + width: 'auto' + }, + '& $brandBar': { + opacity: 0 + } + }, + drawerHeader: { + background: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + padding: '0', + ...theme.mixins.toolbar, + '& h3': { + color: theme.palette.primary.contrastText, + } + }, + avatar: { + margin: 10, + }, + bigAvatar: { + width: 80, + height: 80, + }, + brandBar: { + transition: theme.transitions.create(['width', 'margin', 'background'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + '&:after': { + transition: theme.transitions.create(['box-shadow'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + } + }, + darker: { + background: theme.palette.primary.dark, + }, + title: {}, + nested: { + paddingLeft: 0, + paddingTop: theme.spacing(0.5), + paddingBottom: theme.spacing(0.5), + '& > div > span': { + fontSize: '0.8125rem' + } + }, + child: { + '& a': { + paddingLeft: theme.spacing(3), + } + }, + dense: { + '& > $title:first-child': { + margin: '0' + }, + '& $head': { + paddingLeft: theme.spacing(7) + } + }, + active: { + backgroundColor: theme.palette.primary.light, + '& $primary, & $icon': { + color: theme.palette.secondary.dark, + }, + '&:hover': { + backgroundColor: theme.palette.primary.light, + } + }, + nolist: { + listStyle: 'none', + }, + primary: {}, + iconWrapper: { + width: theme.spacing(5), + minWidth: 0, + marginRight: 0, + marginLeft: theme.spacing(2) + }, + icon: { + marginRight: 0, + color: theme.palette.secondary.dark, + }, + head: { + paddingLeft: 0 + }, + brand: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '10px 10px 5px', + height: 64, + position: 'relative', + '& img': { + width: 20 + }, + '& h3': { + fontSize: 16, + margin: 0, + paddingLeft: 10, + fontWeight: 500 + } + }, + profile: { + height: 120, + display: 'flex', + fontSize: 14, + padding: 10, + alignItems: 'center', + '& h4': { + fontSize: 18, + marginBottom: 0, + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + width: 110 + }, + '& span': { + fontSize: 12, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + width: 110, + display: 'block', + overflow: 'hidden' + } + }, + menuContainer: { + padding: theme.spacing(1), + background: theme.palette.background.default, + [theme.breakpoints.up('lg')]: { + padding: theme.spacing(1.5), + }, + paddingRight: theme.spacing(1), + overflow: 'auto', + height: 'calc(100% - 185px)', + position: 'relative', + display: 'block' + }, + divider: { + marginTop: theme.spacing(1) + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/SocialMedia/Comment.js b/front/odiparpack/app/components/SocialMedia/Comment.js new file mode 100644 index 0000000..9ead6e7 --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/Comment.js @@ -0,0 +1,135 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Type from 'ba-styles/Typography.scss'; +import { withStyles } from '@material-ui/core/styles'; +import Send from '@material-ui/icons/Send'; +import CommentIcon from '@material-ui/icons/Comment'; +import CloseIcon from '@material-ui/icons/Close'; +import dummy from 'ba-api/dummyContents'; +import { + Typography, + List, + ListItem, + Avatar, + Input, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Fab, + Slide, + Divider, + withMobileDialog, +} from '@material-ui/core'; +import styles from './jss/socialMedia-jss'; + +const Transition = React.forwardRef(function Transition(props, ref) { // eslint-disable-line + return <Slide direction="up" ref={ref} {...props} />; +}); + +class Comment extends React.Component { + state = { + comment: '' + }; + + handleChange = event => { + this.setState({ comment: event.target.value }); + }; + + handleSubmit = comment => { + this.props.submitComment(comment); + this.setState({ comment: '' }); + } + + render() { + const { + open, + handleClose, + classes, + dataComment, + fullScreen + } = this.props; + const { comment } = this.state; + const getItem = dataArray => dataArray.map(data => ( + <Fragment key={data.get('id')}> + <ListItem> + <div className={classes.commentContent}> + <div className={classes.commentHead}> + <Avatar alt="avatar" src={data.get('avatar')} className={classes.avatar} /> + <section> + <Typography variant="subtitle1">{data.get('from')}</Typography> + <Typography variant="caption"><span className={classNames(Type.light, Type.textGrey)}>{data.get('date')}</span></Typography> + </section> + </div> + <Typography className={classes.commentText}>{data.get('message')}</Typography> + </div> + </ListItem> + <Divider variant="inset" /> + </Fragment> + )); + + return ( + <div> + <Dialog + fullScreen={fullScreen} + open={open} + onClose={handleClose} + aria-labelledby="form-dialog-title" + TransitionComponent={Transition} + maxWidth="md" + > + <DialogTitle id="form-dialog-title"> + <CommentIcon /> + {' '} + {dataComment !== undefined && dataComment.size} + Comment + {dataComment !== undefined && dataComment.size > 1 ? 's' : ''} + <IconButton onClick={handleClose} className={classes.buttonClose} aria-label="Close"> + <CloseIcon /> + </IconButton> + </DialogTitle> + <DialogContent> + <List> + {dataComment !== undefined && getItem(dataComment)} + </List> + </DialogContent> + <DialogActions className={classes.commentAction}> + <div className={classes.commentForm}> + <Avatar alt="avatar" src={dummy.user.avatar} className={classes.avatarMini} /> + <Input + placeholder="Write Comment" + onChange={this.handleChange} + value={comment} + className={classes.input} + inputProps={{ + 'aria-label': 'Comment', + }} + /> + <Fab size="small" onClick={() => this.handleSubmit(comment)} color="secondary" aria-label="send" className={classes.button}> + <Send /> + </Fab> + </div> + </DialogActions> + </Dialog> + </div> + ); + } +} + +Comment.propTypes = { + open: PropTypes.bool.isRequired, + handleClose: PropTypes.func.isRequired, + submitComment: PropTypes.func.isRequired, + classes: PropTypes.object.isRequired, + dataComment: PropTypes.object, + fullScreen: PropTypes.bool.isRequired, +}; + +Comment.defaultProps = { + dataComment: undefined +}; + +const CommentResponsive = withMobileDialog()(Comment); +export default withStyles(styles)(CommentResponsive); diff --git a/front/odiparpack/app/components/SocialMedia/Cover.js b/front/odiparpack/app/components/SocialMedia/Cover.js new file mode 100644 index 0000000..4823f13 --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/Cover.js @@ -0,0 +1,103 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import VerifiedUser from '@material-ui/icons/VerifiedUser'; +import Info from '@material-ui/icons/Info'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { withStyles } from '@material-ui/core/styles'; +import { Avatar, Typography, Menu, MenuItem, Button, IconButton } from '@material-ui/core'; +import styles from './jss/cover-jss'; + + +const optionsOpt = [ + 'Edit Profile', + 'Change Cover', + 'Option 1', + 'Option 2', + 'Option 3', +]; + +const ITEM_HEIGHT = 48; + +class Cover extends React.Component { + state = { + anchorElOpt: null, + }; + + handleClickOpt = event => { + this.setState({ anchorElOpt: event.currentTarget }); + }; + + handleCloseOpt = () => { + this.setState({ anchorElOpt: null }); + }; + + render() { + const { + classes, + avatar, + name, + desc, + coverImg, + } = this.props; + const { anchorElOpt } = this.state; + return ( + <div className={classes.cover} style={{ backgroundImage: `url(${coverImg})` }}> + <div className={classes.opt}> + <IconButton className={classes.button} aria-label="Delete"> + <Info /> + </IconButton> + <IconButton + aria-label="More" + aria-owns={anchorElOpt ? 'long-menu' : null} + aria-haspopup="true" + className={classes.button} + onClick={this.handleClickOpt} + > + <MoreVertIcon /> + </IconButton> + <Menu + id="long-menu" + anchorEl={anchorElOpt} + open={Boolean(anchorElOpt)} + onClose={this.handleCloseOpt} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: 200, + }, + }} + > + {optionsOpt.map(option => ( + <MenuItem key={option} selected={option === 'Edit Profile'} onClick={this.handleCloseOpt}> + {option} + </MenuItem> + ))} + </Menu> + </div> + <div className={classes.content}> + <Avatar alt={name} src={avatar} className={classes.avatar} /> + <Typography variant="h4" className={classes.name} gutterBottom> + {name} + <VerifiedUser className={classes.verified} /> + </Typography> + <Typography className={classes.subheading} gutterBottom> + {desc} + </Typography> + <Button className={classes.button} size="large" variant="contained" color="secondary"> + Add to Connection + </Button> + </div> + </div> + ); + } +} + +Cover.propTypes = { + classes: PropTypes.object.isRequired, + avatar: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + desc: PropTypes.string.isRequired, + coverImg: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(Cover); diff --git a/front/odiparpack/app/components/SocialMedia/SideSection.js b/front/odiparpack/app/components/SocialMedia/SideSection.js new file mode 100644 index 0000000..1fe8d9c --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/SideSection.js @@ -0,0 +1,209 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; +import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; +import SwipeableViews from 'react-swipeable-views'; +import imgApi from 'ba-api/images'; +import avatarApi from 'ba-api/avatars'; +import { + Grid, + Typography, + MobileStepper, + Paper, + Avatar, + Button, + Divider, + List, + ListItem, + ListItemText, +} from '@material-ui/core'; +import PapperBlock from '../PapperBlock/PapperBlock'; +import NewsCard from '../CardPaper/NewsCard'; +import ProfileCard from '../CardPaper/ProfileCard'; +import styles from './jss/socialMedia-jss'; + + +const slideData = [ + { + label: 'How to be happy :)', + imgPath: imgApi[49], + }, + { + label: '1. Work with something that you like, like…', + imgPath: imgApi[17], + }, + { + label: '2. Keep your friends close to you and hangout with them', + imgPath: imgApi[34], + }, + { + label: '3. Travel everytime that you have a chance', + imgPath: imgApi[10], + }, + { + label: '4. And contribute to Material-UI :D', + imgPath: imgApi[40] + }, +]; + +class SideSection extends React.Component { + state = { + activeStepSwipe: 0, + }; + + handleNextSwipe = () => { + this.setState(prevState => ({ + activeStepSwipe: prevState.activeStepSwipe + 1, + })); + }; + + handleBackSwipe = () => { + this.setState(prevState => ({ + activeStepSwipe: prevState.activeStepSwipe - 1, + })); + }; + + handleStepChangeSwipe = activeStepSwipe => { + this.setState({ activeStepSwipe }); + }; + + render() { + const { classes, theme } = this.props; + const { activeStepSwipe } = this.state; + + const maxStepsSwipe = slideData.length; + return ( + <div> + {/* Profile */} + <ProfileCard + cover={imgApi[43]} + avatar={avatarApi[6]} + name="John Doe" + title="UX designer" + connection={10} + btnText="My Profile" + isVerified + /> + <Divider className={classes.divider} /> + {/* ----------------------------------------------------------------------*/} + {/* News Or Ads Block */} + <Paper> + <SwipeableViews + axis={theme.direction === 'rtl' ? 'x-reverse' : 'x'} + index={this.state.activeStepSwipe} + onChangeIndex={this.handleStepChangeSwipe} + enableMouseEvents + className={classes.sliderWrap} + > + {slideData.map((slide, index) => ( + <div className={classes.figure} key={index.toString()}> + <NewsCard + image={slide.imgPath} + title="slide.label" + > + <Typography gutterBottom className={classes.title} variant="h5" component="h2"> + {slide.label} + </Typography> + </NewsCard> + </div> + ))} + </SwipeableViews> + <MobileStepper + variant="dots" + steps={maxStepsSwipe} + position="static" + activeStep={activeStepSwipe} + className={classes.mobileStepper} + nextButton={( + <Button size="small" onClick={this.handleNextSwipe} disabled={activeStepSwipe === maxStepsSwipe - 1}> + Next + {theme.direction === 'rtl' ? <KeyboardArrowLeft /> : <KeyboardArrowRight />} + </Button> + )} + backButton={( + <Button size="small" onClick={this.handleBackSwipe} disabled={activeStepSwipe === 0}> + {theme.direction === 'rtl' ? <KeyboardArrowRight /> : <KeyboardArrowLeft />} + Back + </Button> + )} + /> + </Paper> + {/* ----------------------------------------------------------------------*/} + {/* People */} + <PapperBlock title="People You May Know" whiteBg noMargin desc=""> + <List component="nav" dense className={classes.profileList}> + <ListItem button className={classes.noPadding}> + <Avatar className={classNames(classes.avatar, classes.orangeAvatar)}>H</Avatar> + <ListItemText primary="Harry Wells" secondary="2 Mutual Connection" /> + <Button color="secondary" size="small">Connect</Button> + </ListItem> + <ListItem button className={classes.noPadding}> + <Avatar className={classNames(classes.avatar, classes.purpleAvatar)}>J</Avatar> + <ListItemText primary="John Doe" secondary="8 Mutual Connection" /> + <Button color="secondary" size="small">Connect</Button> + </ListItem> + <ListItem button className={classes.noPadding}> + <Avatar className={classNames(classes.avatar, classes.pinkAvatar)}>V</Avatar> + <ListItemText primary="Victor Wanggai" secondary="12 Mutual Connection" /> + <Button color="secondary" size="small">Connect</Button> + </ListItem> + <ListItem button className={classes.noPadding}> + <Avatar className={classNames(classes.avatar, classes.greenAvatar)}>H</Avatar> + <ListItemText primary="Baron Phoenix" secondary="10 Mutual Connection" /> + <Button color="secondary" size="small">Connect</Button> + </ListItem> + </List> + <Divider className={classes.divider} /> + <Grid container justify="center"> + <Button color="secondary" className={classes.button}> + See All + </Button> + </Grid> + </PapperBlock> + {/* ----------------------------------------------------------------------*/} + {/* Trending */} + <PapperBlock title="Trends For You" whiteBg desc=""> + <List dense className={classes.trendingList}> + <ListItem className={classes.noPadding}> + <a href="#" className={classes.link}>#Lorem ipsum dolor</a> + <ListItemText secondary="2987 Posts" /> + </ListItem> + <ListItem className={classes.noPadding}> + <a href="#" className={classes.link}>#Aliquam venenatis</a> + <ListItemText secondary="2345 Posts" /> + </ListItem> + <ListItem className={classes.noPadding}> + <a href="#" className={classes.link}>#Nam sollicitudin</a> + <ListItemText secondary="1234 Posts" /> + </ListItem> + <ListItem className={classes.noPadding}> + <a href="#" className={classes.link}>#Cras convallis</a> + <ListItemText secondary="6789 Connection" /> + </ListItem> + <ListItem className={classes.noPadding}> + <a href="#" className={classes.link}>#Aenean sit amet</a> + <ListItemText secondary="2987 Connection" /> + </ListItem> + <ListItem className={classes.noPadding}> + <a href="#" className={classes.link}>#Quisque</a> + <ListItemText secondary="1456 Connection" /> + </ListItem> + <ListItem className={classes.noPadding}> + <a href="#" className={classes.link}>#Lorem ipusm dolor</a> + <ListItemText secondary="2987 Connection" /> + </ListItem> + </List> + </PapperBlock> + </div> + ); + } +} + +SideSection.propTypes = { + classes: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, +}; + +export default withStyles(styles, { withTheme: true })(SideSection); diff --git a/front/odiparpack/app/components/SocialMedia/Timeline.js b/front/odiparpack/app/components/SocialMedia/Timeline.js new file mode 100644 index 0000000..e46826b --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/Timeline.js @@ -0,0 +1,177 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import FavoriteIcon from '@material-ui/icons/Favorite'; +import ShareIcon from '@material-ui/icons/Share'; +import CommentIcon from '@material-ui/icons/Comment'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { + Typography, + Card, + Menu, + MenuItem, + CardHeader, + CardMedia, + CardContent, + CardActions, + IconButton, + Icon, + Avatar, + Tooltip, +} from '@material-ui/core'; +import Comment from './Comment'; +import styles from './jss/timeline-jss'; + + +const optionsOpt = [ + 'Option 1', + 'Option 2', + 'Option 3', +]; + +const ITEM_HEIGHT = 48; + +class Timeline extends React.Component { + state = { + anchorElOpt: null, + openComment: false, + }; + + handleClickOpt = event => { + this.setState({ anchorElOpt: event.currentTarget }); + }; + + handleCloseOpt = () => { + this.setState({ anchorElOpt: null }); + }; + + handleOpenComment = (data) => { + this.props.fetchComment(data); + this.setState({ openComment: true }); + }; + + handleCloseComment = () => { + this.setState({ openComment: false }); + }; + + render() { + const { + classes, + dataTimeline, + onlike, + commentIndex, + submitComment, + } = this.props; + const { anchorElOpt, openComment } = this.state; + const getItem = dataArray => dataArray.map(data => ( + <li key={data.get('id')}> + <div className={classes.iconBullet}> + <Tooltip id={'tooltip-icon-' + data.get('id')} title={data.get('time')}> + <Icon className={classes.icon}> + {data.get('icon')} + </Icon> + </Tooltip> + </div> + <Card className={classes.cardSocmed}> + <CardHeader + avatar={ + <Avatar alt="avatar" src={data.get('avatar')} className={classes.avatar} /> + } + action={( + <IconButton + aria-label="More" + aria-owns={anchorElOpt ? 'long-menu' : null} + aria-haspopup="true" + className={classes.button} + onClick={this.handleClickOpt} + > + <MoreVertIcon /> + </IconButton> + )} + title={data.get('name')} + subheader={data.get('date')} + /> + { data.get('image') !== '' + && ( + <CardMedia + className={classes.media} + image={data.get('image')} + title={data.get('name')} + /> + ) + } + <CardContent> + <Typography component="p"> + {data.get('content')} + </Typography> + </CardContent> + <CardActions className={classes.actions}> + <IconButton aria-label="Like this" onClick={() => onlike(data)}> + <FavoriteIcon className={data.get('liked') ? classes.liked : ''} /> + </IconButton> + <IconButton aria-label="Share"> + <ShareIcon /> + </IconButton> + <div className={classes.rightIcon}> + <Typography variant="caption" component="span"> + {data.get('comments') !== undefined ? data.get('comments').size : 0} + </Typography> + <IconButton aria-label="Comment" onClick={() => this.handleOpenComment(data)}> + <CommentIcon /> + </IconButton> + </div> + </CardActions> + </Card> + </li> + )); + return ( + <Fragment> + <Menu + id="long-menu" + anchorEl={anchorElOpt} + open={Boolean(anchorElOpt)} + onClose={this.handleCloseOpt} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: 200, + }, + }} + > + {optionsOpt.map(option => ( + <MenuItem key={option} selected={option === 'Edit Profile'} onClick={this.handleCloseOpt}> + {option} + </MenuItem> + ))} + </Menu> + <Comment + open={openComment} + handleClose={this.handleCloseComment} + submitComment={submitComment} + dataComment={dataTimeline.getIn([commentIndex, 'comments'])} + /> + <ul className={classes.timeline}> + {getItem(dataTimeline)} + </ul> + </Fragment> + ); + } +} + +Timeline.propTypes = { + classes: PropTypes.object.isRequired, + onlike: PropTypes.func, + dataTimeline: PropTypes.object.isRequired, + fetchComment: PropTypes.func, + submitComment: PropTypes.func, + commentIndex: PropTypes.number, +}; + +Timeline.defaultProps = { + onlike: () => (false), + fetchComment: () => {}, + submitComment: () => {}, + commentIndex: 0, +}; + +export default withStyles(styles)(Timeline); diff --git a/front/odiparpack/app/components/SocialMedia/WritePost.js b/front/odiparpack/app/components/SocialMedia/WritePost.js new file mode 100644 index 0000000..3452aea --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/WritePost.js @@ -0,0 +1,169 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Dropzone from 'react-dropzone'; +import { withStyles } from '@material-ui/core/styles'; +import PhotoCamera from '@material-ui/icons/PhotoCamera'; +import Send from '@material-ui/icons/Send'; +import ActionDelete from '@material-ui/icons/Delete'; +import dummy from 'ba-api/dummyContents'; +import { IconButton, Fab, MenuItem, FormControl, Avatar, Paper, Select, Tooltip } from '@material-ui/core'; +import styles from './jss/writePost-jss'; + + +function isImage(file) { + const fileName = file.name || file.path; + const suffix = fileName.substr(fileName.indexOf('.') + 1).toLowerCase(); + if (suffix === 'jpg' || suffix === 'jpeg' || suffix === 'bmp' || suffix === 'png') { + return true; + } + return false; +} + +class WritePost extends React.Component { + constructor(props) { + super(props); + this.state = { + privacy: 'public', + files: [], + message: '' + }; + this.onDrop = this.onDrop.bind(this); + } + + onDrop(filesVal) { + const { files } = this.state; + let oldFiles = files; + const filesLimit = 2; + oldFiles = oldFiles.concat(filesVal); + if (oldFiles.length > filesLimit) { + console.log('Cannot upload more than ' + filesLimit + ' items.'); + } else { + this.setState({ files: filesVal }); + } + } + + handleRemove(file, fileIndex) { + const thisFiles = this.state.files; + // This is to prevent memory leaks. + window.URL.revokeObjectURL(file.preview); + + thisFiles.splice(fileIndex, 1); + this.setState({ files: thisFiles }); + } + + handleChange = event => { + this.setState({ privacy: event.target.value }); + }; + + handleWrite = event => { + this.setState({ message: event.target.value }); + }; + + handlePost = (message, files, privacy) => { + // Submit Post to reducer + this.props.submitPost(message, files, privacy); + // Reset all fields + this.setState({ + privacy: 'public', + files: [], + message: '' + }); + } + + render() { + const { classes } = this.props; + let dropzoneRef; + const { privacy, files, message } = this.state; + const acceptedFiles = ['image/jpeg', 'image/png', 'image/bmp']; + const fileSizeLimit = 3000000; + const deleteBtn = (file, index) => ( + <div className={classNames(classes.removeBtn, 'middle')}> + <IconButton onClick={() => this.handleRemove(file, index)}> + <ActionDelete className="removeBtn" /> + </IconButton> + </div> + ); + const previews = filesArray => filesArray.map((file, index) => { + const path = URL.createObjectURL(file) || '/pic' + file.path; + if (isImage(file)) { + return ( + <div key={index.toString()}> + <figure><img src={path} alt="preview" /></figure> + {deleteBtn(file, index)} + </div> + ); + } + return false; + }); + return ( + <div className={classes.statusWrap}> + <Paper> + <Avatar alt="avatar" src={dummy.user.avatar} className={classes.avatarMini} /> + <textarea + row="2" + placeholder="What's on your mind?" + value={message} + onChange={this.handleWrite} + /> + <Dropzone + className={classes.hiddenDropzone} + accept={acceptedFiles.join(',')} + acceptClassName="stripes" + onDrop={this.onDrop} + maxSize={fileSizeLimit} + ref={(node) => { dropzoneRef = node; }} + > + {({ getRootProps, getInputProps }) => ( + <div {...getRootProps()}> + <input {...getInputProps()} /> + </div> + )} + </Dropzone> + <div className={classes.preview}> + {previews(files)} + </div> + <div className={classes.control}> + <Tooltip id="tooltip-upload" title="Upload Photo"> + <IconButton + className={classes.button} + component="button" + onClick={() => { + dropzoneRef.open(); + }} + > + <PhotoCamera /> + </IconButton> + </Tooltip> + <div className={classes.privacy}> + <FormControl className={classes.formControl}> + <Select + value={privacy} + onChange={this.handleChange} + name="privacy" + className={classes.selectEmpty} + > + <MenuItem value="public">Public</MenuItem> + <MenuItem value="friends">Friends</MenuItem> + <MenuItem value="private">Only Me</MenuItem> + </Select> + </FormControl> + </div> + <Tooltip id="tooltip-post" title="Post"> + <Fab onClick={() => this.handlePost(message, files, privacy)} size="small" color="secondary" aria-label="send" className={classes.sendBtn}> + <Send /> + </Fab> + </Tooltip> + </div> + </Paper> + </div> + ); + } +} + +WritePost.propTypes = { + classes: PropTypes.object.isRequired, + submitPost: PropTypes.func.isRequired +}; + +export default withStyles(styles)(WritePost); diff --git a/front/odiparpack/app/components/SocialMedia/jss/cover-jss.js b/front/odiparpack/app/components/SocialMedia/jss/cover-jss.js new file mode 100644 index 0000000..695512d --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/jss/cover-jss.js @@ -0,0 +1,55 @@ +import { fade } from '@material-ui/core/styles/colorManipulator'; +const styles = theme => ({ + root: { + flexGrow: 1, + }, + cover: { + '& $name, & $subheading': { + color: theme.palette.common.white + }, + position: 'relative', + width: '100%', + overflow: 'hidden', + height: 360, + backgroundColor: theme.palette.primary.main, + display: 'flex', + justifyContent: 'center', + alignItems: 'flex-end', + borderRadius: 2, + backgroundSize: 'cover', + textAlign: 'center', + boxShadow: theme.shadows[7] + }, + content: { + background: fade(theme.palette.secondary.main, 0.3), + height: '100%', + width: '100%', + padding: `70px ${theme.spacing(3)}px 30px` + }, + name: {}, + subheading: {}, + avatar: { + margin: '0 auto', + width: 120, + height: 120, + border: '3px solid rgba(255, 255, 255, .5)' + }, + opt: { + position: 'absolute', + top: 10, + right: 10, + '& button': { + color: theme.palette.common.white + } + }, + verified: { + margin: theme.spacing(1), + top: 10, + position: 'relative' + }, + button: { + marginTop: theme.spacing(1) + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/SocialMedia/jss/socialMedia-jss.js b/front/odiparpack/app/components/SocialMedia/jss/socialMedia-jss.js new file mode 100644 index 0000000..03f5726 --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/jss/socialMedia-jss.js @@ -0,0 +1,89 @@ +import { deepOrange, deepPurple, pink, green } from '@material-ui/core/colors'; + +const styles = theme => ({ + mobileStepper: { + margin: `0 auto ${theme.spacing(4)}px`, + textAlign: 'center' + }, + avatar: { + marginRight: 15, + }, + orangeAvatar: { + backgroundColor: deepOrange[500], + }, + purpleAvatar: { + backgroundColor: deepPurple[500], + }, + pinkAvatar: { + backgroundColor: pink[500], + }, + greenAvatar: { + backgroundColor: green[500], + }, + divider: { + margin: `${theme.spacing(2)}px 0`, + background: 'none' + }, + link: { + color: theme.palette.primary.main + }, + noPadding: { + padding: '5px', + marginLeft: -10 + }, + sliderWrap: { + height: 310, + overflow: 'hidden' + }, + title: { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + fontSize: 18 + }, + profileList: {}, + trendingList: { + '& li': { + display: 'block' + } + }, + input: {}, + commentContent: { + padding: 10 + }, + commentText: { + marginTop: 5 + }, + buttonClose: { + position: 'absolute', + top: 20, + right: 20 + }, + avatarMini: { + width: 30, + height: 30, + }, + commentAction: { + background: theme.palette.grey[100], + margin: 0, + }, + commentForm: { + display: 'flex', + alignItems: 'center', + [theme.breakpoints.up('md')]: { + minWidth: 600, + }, + width: '100%', + padding: '15px 20px', + margin: 0, + '& $input': { + flex: 1, + margin: '0 10px' + } + }, + commentHead: { + display: 'flex' + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/SocialMedia/jss/timeline-jss.js b/front/odiparpack/app/components/SocialMedia/jss/timeline-jss.js new file mode 100644 index 0000000..dcdb018 --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/jss/timeline-jss.js @@ -0,0 +1,95 @@ +import { pink } from '@material-ui/core/colors'; +const styles = theme => ({ + card: { + display: 'flex', + justifyContent: 'space-between' + }, + content: { + flex: '1 0 auto', + }, + cover: { + width: 150, + height: 150, + }, + avatar: { + width: 40, + height: 40 + }, + cardSocmed: { + [theme.breakpoints.up('md')]: { + marginLeft: 90, + minWidth: 400, + }, + marginBottom: theme.spacing(3), + position: 'relative', + }, + media: { + height: 0, + paddingTop: '56.25%', // 16:9 + }, + actions: { + display: 'flex', + }, + expandOpen: { + transform: 'rotate(180deg)', + }, + iconBullet: {}, + icon: {}, + timeline: { + position: 'relative', + '&:before': { + left: 39, + content: '""', + top: 40, + height: '101%', + border: `1px solid ${theme.palette.grey[300]}`, + position: 'absolute', + [theme.breakpoints.down('sm')]: { + display: 'none' + }, + }, + '& li': { + position: 'relative', + display: 'block' + }, + '& time': { + top: 70, + left: 20, + position: 'absolute', + textAlign: 'center', + background: theme.palette.common.white, + boxShadow: theme.shadows[3], + padding: '4px 40px 4px 15px', + borderLeft: `3px solid ${theme.palette.secondary.main}` + }, + '& $iconBullet': { + position: 'absolute', + borderRadius: '50%', + top: 20, + width: 40, + height: 40, + background: theme.palette.secondary.main, + boxShadow: theme.shadows[5], + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + left: 20, + '& $icon': { + color: theme.palette.common.white, + }, + [theme.breakpoints.down('sm')]: { + display: 'none' + }, + }, + }, + rightIcon: { + marginLeft: 'auto', + display: 'flex', + alignItems: 'center' + }, + liked: { + color: pink[500] + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/SocialMedia/jss/writePost-jss.js b/front/odiparpack/app/components/SocialMedia/jss/writePost-jss.js new file mode 100644 index 0000000..cd18422 --- /dev/null +++ b/front/odiparpack/app/components/SocialMedia/jss/writePost-jss.js @@ -0,0 +1,73 @@ +const styles = theme => ({ + statusWrap: { + marginBottom: theme.spacing(3), + '& > div': { + overflow: 'hidden' + }, + '& textarea': { + border: 'none', + padding: '20px 20px 20px 50px', + outline: 'none', + width: '100%', + resize: 'none', + overflow: 'hidden', + height: 50, + transition: theme.transitions.create(['height'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + '&:focus': { + height: 100, + overflow: 'auto', + } + } + }, + avatarMini: { + width: 30, + height: 30, + position: 'absolute', + top: 40, + left: 10 + }, + control: { + padding: '10px 20px 0', + display: 'flex' + }, + privacy: { + flex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + textAlign: 'right', + }, + button: { + margin: theme.spacing(0.5) + }, + sendBtn: { + position: 'relative', + top: 5 + }, + formControl: { + margin: '0 20px', + width: 150, + paddingLeft: 10, + textAlign: 'left', + '&:before, &:after': { + borderBottom: 'none' + } + }, + hiddenDropzone: { + display: 'none' + }, + preview: { + position: 'relative', + '& figure': { + textAlign: 'center' + } + }, + removeBtn: { + opacity: 1 + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/SourceReader/SourceReader.js b/front/odiparpack/app/components/SourceReader/SourceReader.js new file mode 100644 index 0000000..e1303a0 --- /dev/null +++ b/front/odiparpack/app/components/SourceReader/SourceReader.js @@ -0,0 +1,114 @@ +import React, { Component } from 'react'; +import { PropTypes } from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Axios from 'axios'; +import SyntaxHighlighter, { registerLanguage } from 'react-syntax-highlighter/prism-light'; +import jsx from 'react-syntax-highlighter/languages/prism/jsx'; +import themeSource from 'react-syntax-highlighter/styles/prism/xonokai'; +import classNames from 'classnames'; +import Code from '@material-ui/icons/Code'; +import Close from '@material-ui/icons/Close'; +import { Button, LinearProgress, Icon } from '@material-ui/core'; +import codePreview from '../../config/codePreview'; + + +const url = '/api/docs?src='; + +const styles = theme => ({ + button: { + margin: '8px 5px', + }, + iconSmall: { + fontSize: 20, + }, + leftIcon: { + marginRight: theme.spacing(1), + }, + source: { + overflow: 'hidden', + height: 0, + position: 'relative', + transition: 'all .5s', + margin: '0 -10px' + }, + preloader: { + position: 'absolute', + top: 36, + left: 0, + width: '100%' + }, + open: { + height: 'auto', + }, + src: { + textAlign: 'center', + margin: 10, + fontFamily: 'monospace', + '& span': { + fontSize: 14, + marginRight: 5, + top: 3, + position: 'relative' + } + } +}); + +class SourceReader extends Component { + state = { raws: [], open: false, loading: false }; + + sourceOpen = () => { + const name = this.props.componentName; + this.setState({ loading: true }, () => { + Axios.get(url + name).then(result => this.setState({ + raws: result.data.records, + loading: false + })); + this.setState({ open: !this.state.open }); + }); + }; + + render() { + const { raws, open, loading } = this.state; + const { classes } = this.props; + registerLanguage('jsx', jsx); + if (codePreview.enable) { + return ( + <div> + <Button onClick={this.sourceOpen} color="secondary" className={classes.button} size="small"> + { open + ? <Close className={classNames(classes.leftIcon, classes.iconSmall)} /> + : <Code className={classNames(classes.leftIcon, classes.iconSmall)} /> + } + { open ? 'Hide Code' : 'Show Code' } + </Button> + <section className={classNames(classes.source, open ? classes.open : '')}> + <p className={classes.src}> + <Icon className="description">description</Icon> + src/app/ + {this.props.componentName} + </p> + {loading + && <LinearProgress color="secondary" className={classes.preloader} /> + } + {raws.map((raw, index) => ([ + <div key={index.toString()}> + <SyntaxHighlighter language="jsx" style={themeSource} showLineNumbers="true"> + {raw.source.toString()} + </SyntaxHighlighter> + </div> + ]) + )} + </section> + </div> + ); + } + return false; + } +} + +SourceReader.propTypes = { + componentName: PropTypes.string.isRequired, + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(SourceReader); diff --git a/front/odiparpack/app/components/Tables/AdvTable.js b/front/odiparpack/app/components/Tables/AdvTable.js new file mode 100644 index 0000000..acb2803 --- /dev/null +++ b/front/odiparpack/app/components/Tables/AdvTable.js @@ -0,0 +1,206 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import tableStyles from 'ba-styles/Table.scss'; +import { Table, TableBody, TableCell, TableRow, TablePagination, Paper, Checkbox } from '@material-ui/core'; +import EnhancedTableHead from './tableParts/TableHeader'; +import EnhancedTableToolbar from './tableParts/TableToolbar'; + + +const styles = theme => ({ + root: { + width: '100%', + marginTop: theme.spacing(3), + }, + table: { + minWidth: 1020, + }, + tableWrapper: { + overflowX: 'auto', + }, +}); + +class AdvTable extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + order: this.props.order, + orderBy: this.props.orderBy, + selected: this.props.selected, + data: this.props.data.sort((a, b) => (a.calories < b.calories ? -1 : 1)), + page: this.props.page, + rowsPerPage: this.props.rowsPerPage, + defaultPerPage: this.props.defaultPerPage, + filterText: this.props.filterText, + }; + } + + handleRequestSort = (event, property) => { + const orderBy = property; + let order = 'desc'; + + if (this.state.orderBy === property && this.state.order === 'desc') { + order = 'asc'; + } + + const data = order === 'desc' + ? this.state.data.sort((a, b) => (b[orderBy] < a[orderBy] ? -1 : 1)) + : this.state.data.sort((a, b) => (a[orderBy] < b[orderBy] ? -1 : 1)); + + this.setState({ data, order, orderBy }); + }; + + handleSelectAllClick = (event, checked) => { + if (checked) { + this.setState({ selected: this.state.data.map(n => n.id) }); + return; + } + this.setState({ selected: [] }); + }; + + handleClick = (event, id) => { + const { selected } = this.state; + const selectedIndex = selected.indexOf(id); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1), + ); + } + + this.setState({ selected: newSelected }); + }; + + handleChangePage = (event, page) => { + this.setState({ page }); + }; + + handleChangeRowsPerPage = event => { + this.setState({ rowsPerPage: event.target.value }); + }; + + isSelected = id => this.state.selected.indexOf(id) !== -1; + + handleUserInput(value) { + // Show all item first + if (value !== '') { + this.setState({ rowsPerPage: this.state.data.length }); + } else { + this.setState({ rowsPerPage: this.state.defaultPerPage }); + } + + // Show result base on keyword + this.setState({ filterText: value.toLowerCase() }); + } + + render() { + const { classes } = this.props; + const { + data, + order, + orderBy, + selected, + rowsPerPage, + page, + filterText + } = this.state; + const { columnData } = this.props; + const checkcell = true; + const emptyRows = rowsPerPage - Math.min(rowsPerPage, data.length - (page * rowsPerPage)); + const renderCell = (dataArray, keyArray) => keyArray.map((itemCell, index) => ( + <TableCell align={itemCell.numeric ? 'right' : 'left'} key={index.toString()}>{dataArray[itemCell.id]}</TableCell> + )); + return ( + <Paper className={classes.root}> + <EnhancedTableToolbar + numSelected={selected.length} + filterText={filterText} + onUserInput={(event) => this.handleUserInput(event)} + /> + <div className={classes.tableWrapper}> + <Table className={classNames(classes.table, tableStyles.stripped)}> + <EnhancedTableHead + numSelected={selected.length} + order={order} + orderBy={orderBy} + onSelectAllClick={this.handleSelectAllClick} + onRequestSort={this.handleRequestSort} + rowCount={data.length} + columnData={columnData} + checkcell={checkcell} + /> + <TableBody> + {data.slice(page * rowsPerPage, (page * rowsPerPage) + rowsPerPage).map(n => { + const isSelected = this.isSelected(n.id); + if (n.name.toLowerCase().indexOf(filterText) === -1) { + return false; + } + return ( + <TableRow + hover + onClick={event => this.handleClick(event, n.id)} + role="checkbox" + aria-checked={isSelected} + tabIndex={-1} + key={n.id} + selected={isSelected} + > + <TableCell padding="checkbox"> + <Checkbox checked={isSelected} /> + </TableCell> + {renderCell(n, columnData)} + </TableRow> + ); + })} + {emptyRows > 0 && ( + <TableRow style={{ height: 49 * emptyRows }}> + <TableCell colSpan={6} /> + </TableRow> + )} + </TableBody> + </Table> + </div> + <TablePagination + component="div" + rowsPerPageOptions={[5, 10, 25]} + count={data.length} + rowsPerPage={rowsPerPage} + page={page} + backIconButtonProps={{ + 'aria-label': 'Previous Page', + }} + nextIconButtonProps={{ + 'aria-label': 'Next Page', + }} + onChangePage={this.handleChangePage} + onChangeRowsPerPage={this.handleChangeRowsPerPage} + /> + </Paper> + ); + } +} + +AdvTable.propTypes = { + classes: PropTypes.object.isRequired, + data: PropTypes.array.isRequired, + order: PropTypes.string.isRequired, + orderBy: PropTypes.string.isRequired, + selected: PropTypes.array.isRequired, + rowsPerPage: PropTypes.number.isRequired, + page: PropTypes.number.isRequired, + defaultPerPage: PropTypes.number.isRequired, + filterText: PropTypes.string.isRequired, + columnData: PropTypes.array.isRequired, +}; + +export default withStyles(styles)(AdvTable); diff --git a/front/odiparpack/app/components/Tables/CrudTable.js b/front/odiparpack/app/components/Tables/CrudTable.js new file mode 100644 index 0000000..d3dd164 --- /dev/null +++ b/front/odiparpack/app/components/Tables/CrudTable.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import MainTable from './tableParts/MainTable'; + +class CrudTable extends React.Component { + componentDidMount() { + this.props.fetchData(this.props.dataInit, this.props.branch); + } + + render() { + const { + title, + dataTable, + addEmptyRow, + removeRow, + updateRow, + editRow, + finishEditRow, + anchor, + branch + } = this.props; + return ( + <MainTable + title={title} + addEmptyRow={addEmptyRow} + items={dataTable} + removeRow={removeRow} + updateRow={updateRow} + editRow={editRow} + finishEditRow={finishEditRow} + anchor={anchor} + branch={branch} + /> + ); + } +} + +CrudTable.propTypes = { + title: PropTypes.string.isRequired, + anchor: PropTypes.array.isRequired, + dataInit: PropTypes.array.isRequired, + dataTable: PropTypes.object.isRequired, + fetchData: PropTypes.func.isRequired, + addEmptyRow: PropTypes.func.isRequired, + removeRow: PropTypes.func.isRequired, + updateRow: PropTypes.func.isRequired, + editRow: PropTypes.func.isRequired, + finishEditRow: PropTypes.func.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default CrudTable; diff --git a/front/odiparpack/app/components/Tables/CrudTableForm.js b/front/odiparpack/app/components/Tables/CrudTableForm.js new file mode 100644 index 0000000..d2d2ea8 --- /dev/null +++ b/front/odiparpack/app/components/Tables/CrudTableForm.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Form from './tableParts/Form'; +import MainTableForm from './tableParts/MainTableForm'; +import FloatingPanel from './../Panel/FloatingPanel'; + +class CrudTableForm extends React.Component { + componentDidMount() { + this.props.fetchData(this.props.dataInit, this.props.branch); + } + + sendValues = (values) => { + setTimeout(() => { + this.props.submit(values, this.props.branch); + }, 500); + } + + render() { + const { + title, + dataTable, + openForm, + closeForm, + removeRow, + addNew, + editRow, + anchor, + children, + branch, + initValues + } = this.props; + return ( + <div> + <FloatingPanel openForm={openForm} branch={branch} closeForm={closeForm}> + <Form onSubmit={this.sendValues} initValues={initValues} branch={branch}> + {children} + </Form> + </FloatingPanel> + <MainTableForm + title={title} + addNew={addNew} + items={dataTable} + removeRow={removeRow} + editRow={editRow} + anchor={anchor} + branch={branch} + /> + </div> + ); + } +} + +CrudTableForm.propTypes = { + title: PropTypes.string.isRequired, + anchor: PropTypes.array.isRequired, + dataInit: PropTypes.array.isRequired, + dataTable: PropTypes.object.isRequired, + fetchData: PropTypes.func.isRequired, + submit: PropTypes.func.isRequired, + addNew: PropTypes.func.isRequired, + openForm: PropTypes.bool.isRequired, + closeForm: PropTypes.func.isRequired, + removeRow: PropTypes.func.isRequired, + editRow: PropTypes.func.isRequired, + children: PropTypes.node.isRequired, + initValues: PropTypes.object.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default CrudTableForm; diff --git a/front/odiparpack/app/components/Tables/EmptyData.js b/front/odiparpack/app/components/Tables/EmptyData.js new file mode 100644 index 0000000..a59c3d6 --- /dev/null +++ b/front/odiparpack/app/components/Tables/EmptyData.js @@ -0,0 +1,14 @@ +import React from 'react'; +import tableStyles from 'ba-styles/Table.scss'; +import TableIcon from '@material-ui/icons/Apps'; + +function EmptyData() { + return ( + <div className={tableStyles.nodata}> + <TableIcon /> + No Data + </div> + ); +} + +export default EmptyData; diff --git a/front/odiparpack/app/components/Tables/TreeTable.js b/front/odiparpack/app/components/Tables/TreeTable.js new file mode 100644 index 0000000..12eeb19 --- /dev/null +++ b/front/odiparpack/app/components/Tables/TreeTable.js @@ -0,0 +1,190 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import ExpandLess from '@material-ui/icons/KeyboardArrowRight'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import Add from '@material-ui/icons/AddCircle'; +import Remove from '@material-ui/icons/RemoveCircleOutline'; + +import { Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core'; + +const styles = theme => ({ + root: { + width: '100%', + marginTop: theme.spacing(3), + overflowX: 'auto', + }, + table: { + minWidth: 700, + }, + hideRow: { + display: 'none' + }, + anchor: { + cursor: 'pointer' + }, + icon: { + top: 5, + position: 'relative', + left: -5 + } +}); + +let RenderRow = props => { + const { + classes, + toggleTree, + treeOpen, + item, + parent, + arrowMore, + icon, + branch + } = props; + + const keyID = item.id; + const dataBody = Object.keys(item); + const dataBodyVal = Object.values(item); + + const renderIconMore = (iconName) => { + if (iconName === 'arrow') { + return <ExpandMore className={classes.icon} />; + } + return <Remove className={classes.icon} />; + }; + + const renderIconLess = (iconName) => { + if (iconName === 'arrow') { + return <ExpandLess className={classes.icon} />; + } + return <Add className={classes.icon} />; + }; + + const renderCell = (dataArray, parentCell) => dataArray.map((itemCell, index) => { + if (index < 1) { + if (parentCell) { + return ( + <TableCell key={index.toString()} style={{ paddingLeft: (keyID.split('_').length) * 20 }}> + {arrowMore.indexOf(keyID) > -1 ? renderIconMore(icon) : renderIconLess(icon)} + {keyID} + </TableCell> + ); + } + return ( + <TableCell key={index.toString()} style={{ paddingLeft: (keyID.split('_').length) * 20 }}>{keyID}</TableCell> + ); + } + + if (itemCell !== 'child') { + return ( + <TableCell key={index.toString()}>{dataBodyVal[index]}</TableCell> + ); + } + + return false; + }); + + const row = parent ? ( + <TableRow + key={keyID} + className={treeOpen.indexOf(keyID) < 0 && keyID.indexOf('_') > -1 ? classes.hideRow : classes.anchor} + onClick={() => toggleTree(keyID, item.child, branch)} + > + {renderCell(dataBody, true)} + </TableRow> + ) : ( + <TableRow + key={keyID} + className={treeOpen.indexOf(keyID) < 0 && keyID.indexOf('_') > -1 ? classes.hideRow : ''} + > + {renderCell(dataBody, false)} + </TableRow> + ); + + return [row]; +}; + +RenderRow.propTypes = { + classes: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, + parent: PropTypes.bool.isRequired, + toggleTree: PropTypes.func.isRequired, + treeOpen: PropTypes.object.isRequired, + arrowMore: PropTypes.object.isRequired, + branch: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired +}; + +RenderRow = withStyles(styles)(RenderRow); + +class TreeTable extends React.Component { + render() { + const { + classes, + dataTable, + icon, + treeOpen, + arrowMore, + toggleTree, + branch + } = this.props; + const parentRow = true; + const getData = dataArray => dataArray.map((item, index) => { + if (item.child) { + return [ + <RenderRow + icon={icon} + treeOpen={treeOpen} + arrowMore={arrowMore} + toggleTree={toggleTree} + item={item} + key={index.toString()} + parent={parentRow} + branch={branch} + />, + getData(item.child) + ]; + } + return ( + <RenderRow + icon={icon} + item={item} + treeOpen={treeOpen} + arrowMore={arrowMore} + toggleTree={toggleTree} + key={index.toString()} + branch={branch} + parent={false} + /> + ); + }); + + const getHead = dataArray => dataArray.map((item, index) => <TableCell key={index.toString()}>{item.label}</TableCell> + ); + + return ( + <Table className={classes.table}> + <TableHead> + <TableRow> + { getHead(dataTable.head) } + </TableRow> + </TableHead> + <TableBody> + { getData(dataTable.body) } + </TableBody> + </Table> + ); + } +} + +TreeTable.propTypes = { + classes: PropTypes.object.isRequired, + dataTable: PropTypes.object.isRequired, + treeOpen: PropTypes.object.isRequired, + toggleTree: PropTypes.func.isRequired, + arrowMore: PropTypes.object.isRequired, + branch: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired +}; + +export default withStyles(styles)(TreeTable); diff --git a/front/odiparpack/app/components/Tables/tableParts/DatePickerCell.js b/front/odiparpack/app/components/Tables/tableParts/DatePickerCell.js new file mode 100644 index 0000000..161d0eb --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/DatePickerCell.js @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { KeyboardDatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import MomentUtils from '@date-io/moment'; +import css from 'ba-styles/Table.scss'; +import { TableCell } from '@material-ui/core'; + +class DatePickerCell extends React.Component { + state = { + event: { + target: { + name: this.props.cellData.type, // eslint-disable-line + value: this.props.cellData.value, // eslint-disable-line + } + } + } + + handleDateChange = date => { + const { event } = this.state; + const { branch, updateRow } = this.props; + event.target.value = date; + updateRow(event, branch); + } + + render() { + const { + edited, + cellData + } = this.props; + const { event } = this.state; + return ( + <TableCell padding="none" className="text-center" textalign="center"> + <MuiPickersUtilsProvider utils={MomentUtils}> + <KeyboardDatePicker + clearable + name={cellData.type} + className={css.crudInput} + format="DD/MM/YYYY" + placeholder="10/10/2018" + mask={[/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/]} + value={event.target.value} + disabled={!edited} + onChange={this.handleDateChange} + animateYearScrolling={false} + /> + </MuiPickersUtilsProvider> + </TableCell> + ); + } +} + +DatePickerCell.propTypes = { + cellData: PropTypes.object.isRequired, + updateRow: PropTypes.func.isRequired, + edited: PropTypes.bool.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default DatePickerCell; diff --git a/front/odiparpack/app/components/Tables/tableParts/EditableCell.js b/front/odiparpack/app/components/Tables/tableParts/EditableCell.js new file mode 100644 index 0000000..2c7ba8f --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/EditableCell.js @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import css from 'ba-styles/Table.scss'; + +import { TableCell, Input, TextField } from '@material-ui/core'; + +class EditableCell extends React.Component { + handleUpdate(event) { + event.persist(); + this.props.updateRow(event, this.props.branch); + } + + render() { + const { + cellData, + edited, + inputType + } = this.props; + switch (inputType) { + case 'text': + return ( + <TableCell padding="none"> + <Input + placeholder={cellData.type} + name={cellData.type} + className={css.crudInput} + id={cellData.id.toString()} + value={cellData.value} + onChange={(event) => this.handleUpdate(event)} + disabled={!edited} + margin="none" + inputProps={{ + 'aria-label': 'Description', + }} + /> + </TableCell> + ); + case 'number': + return ( + <TableCell padding="none"> + <TextField + id={cellData.id.toString()} + name={cellData.type} + className={css.crudInput} + value={cellData.value} + onChange={(event) => this.handleUpdate(event)} + type="number" + InputLabelProps={{ + shrink: true, + }} + margin="none" + disabled={!edited} + /> + </TableCell> + ); + default: + return ( + <TableCell padding="none"> + <Input + placeholder={cellData.type} + name={cellData.type} + className={css.crudInput} + id={cellData.id.toString()} + value={cellData.value} + onChange={(event) => this.handleUpdate(event)} + disabled={!edited} + margin="none" + inputProps={{ + 'aria-label': 'Description', + }} + /> + </TableCell> + ); + } + } +} + +EditableCell.propTypes = { + inputType: PropTypes.string.isRequired, + cellData: PropTypes.object.isRequired, + updateRow: PropTypes.func.isRequired, + edited: PropTypes.bool.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default EditableCell; diff --git a/front/odiparpack/app/components/Tables/tableParts/Form.js b/front/odiparpack/app/components/Tables/tableParts/Form.js new file mode 100644 index 0000000..da66966 --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/Form.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { reduxForm } from 'redux-form/immutable'; +import css from 'ba-styles/Form.scss'; +import { Button } from '@material-ui/core'; + +class Form extends Component { + componentDidMount() { + // this.ref // the Field + // .getRenderedComponent() // on Field, returns ReduxFormMaterialUITextField + // .getRenderedComponent() // on ReduxFormMaterialUITextField, returns TextField + // .focus() // on TextField + // console.log(this.props.initValues); + } + + render() { + const { + handleSubmit, + children, + reset, + pristine, + submitting, + } = this.props; + + return ( + <div> + <form onSubmit={handleSubmit}> + <section className={css.bodyForm}> + {children} + </section> + <div className={css.buttonArea}> + <Button variant="contained" color="secondary" type="submit" disabled={submitting}> + Submit + </Button> + <Button + type="button" + disabled={pristine || submitting} + onClick={reset} + > + Reset + </Button> + </div> + </form> + </div> + ); + } +} + +Form.propTypes = { + children: PropTypes.node.isRequired, + handleSubmit: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, +}; + +const FormMapped = reduxForm({ + form: 'immutableExample', + enableReinitialize: true, +})(Form); + + +const FormMappedInit = connect( + state => ({ + initialValues: state.getIn(['crudTableForm', 'formValues']) + }) +)(FormMapped); + + +export default FormMappedInit; diff --git a/front/odiparpack/app/components/Tables/tableParts/MainTable.js b/front/odiparpack/app/components/Tables/tableParts/MainTable.js new file mode 100644 index 0000000..973bccf --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/MainTable.js @@ -0,0 +1,104 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import AddIcon from '@material-ui/icons/Add'; +import css from 'ba-styles/Table.scss'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Toolbar, + Typography, + Tooltip, + Button, +} from '@material-ui/core'; +import Row from './Row'; +import styles from './tableStyle-jss'; + + +class MainTable extends React.Component { + render() { + const { + classes, + items, + addEmptyRow, + removeRow, + updateRow, + editRow, + finishEditRow, + anchor, + branch, + title + } = this.props; + + const getItems = dataArray => dataArray.map(item => ( + <Row + anchor={anchor} + updateRow={(event) => updateRow(event, item, branch)} + item={item} + removeRow={() => removeRow(item, branch)} + key={item.get('id')} + editRow={() => editRow(item, branch)} + finishEditRow={() => finishEditRow(item, branch)} + branch={branch} + /> + )); + + const getHead = dataArray => dataArray.map((item, index) => { + if (!item.hidden) { + return ( + <TableCell padding="none" key={index.toString()} width={item.width}>{item.label}</TableCell> + ); + } + return false; + }); + return ( + <div> + <Toolbar className={classes.toolbar}> + <div className={classes.title}> + <Typography variant="h6">{title}</Typography> + </div> + <div className={classes.spacer} /> + <div className={classes.actions}> + <Tooltip title="Add Item"> + <Button variant="contained" onClick={() => addEmptyRow(anchor, branch)} color="secondary" className={classes.button}> + <AddIcon className={classNames(classes.leftIcon, classes.iconSmall)} /> + Add New + </Button> + </Tooltip> + </div> + </Toolbar> + <div className={classes.rootTable}> + <Table className={classNames(css.tableCrud, classes.table, css.stripped)}> + <TableHead> + <TableRow> + { getHead(anchor) } + </TableRow> + </TableHead> + <TableBody> + {getItems(items)} + </TableBody> + </Table> + </div> + </div> + ); + } +} + +MainTable.propTypes = { + title: PropTypes.string.isRequired, + classes: PropTypes.object.isRequired, + items: PropTypes.object.isRequired, + anchor: PropTypes.array.isRequired, + addEmptyRow: PropTypes.func.isRequired, + removeRow: PropTypes.func.isRequired, + updateRow: PropTypes.func.isRequired, + editRow: PropTypes.func.isRequired, + finishEditRow: PropTypes.func.isRequired, + branch: PropTypes.string.isRequired +}; + +export default withStyles(styles)(MainTable); diff --git a/front/odiparpack/app/components/Tables/tableParts/MainTableForm.js b/front/odiparpack/app/components/Tables/tableParts/MainTableForm.js new file mode 100644 index 0000000..ccf0e4a --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/MainTableForm.js @@ -0,0 +1,97 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import AddIcon from '@material-ui/icons/Add'; +import css from 'ba-styles/Table.scss'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Toolbar, + Typography, + Tooltip, + Button, +} from '@material-ui/core'; +import RowReadOnly from './RowReadOnly'; +import styles from './tableStyle-jss'; + + +class MainTableForm extends React.Component { + render() { + const { + title, + classes, + items, + removeRow, + editRow, + addNew, + anchor, + branch + } = this.props; + const getItems = dataArray => dataArray.map(item => ( + <RowReadOnly + item={item} + removeRow={() => removeRow(item, branch)} + key={item.get('id')} + editRow={() => editRow(item, branch)} + anchor={anchor} + branch={branch} + /> + )); + + const getHead = dataArray => dataArray.map((item, index) => { + if (!item.hidden) { + return ( + <TableCell padding="none" key={index.toString()} width={item.width}>{item.label}</TableCell> + ); + } + return false; + }); + return ( + <div> + <Toolbar className={classes.toolbar}> + <div className={classes.title}> + <Typography variant="h6">{title}</Typography> + </div> + <div className={classes.spacer} /> + <div className={classes.actions}> + <Tooltip title="Add Item"> + <Button variant="contained" onClick={() => addNew(anchor, branch)} color="secondary" className={classes.button}> + <AddIcon className={classNames(classes.leftIcon, classes.iconSmall)} /> + Add New + </Button> + </Tooltip> + </div> + </Toolbar> + <div className={classes.rootTable}> + <Table className={classNames(css.tableCrud, classes.table, css.stripped)}> + <TableHead> + <TableRow> + { getHead(anchor) } + </TableRow> + </TableHead> + <TableBody> + {getItems(items)} + </TableBody> + </Table> + </div> + </div> + ); + } +} + +MainTableForm.propTypes = { + title: PropTypes.string.isRequired, + classes: PropTypes.object.isRequired, + items: PropTypes.object.isRequired, + anchor: PropTypes.array.isRequired, + addNew: PropTypes.func.isRequired, + removeRow: PropTypes.func.isRequired, + editRow: PropTypes.func.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(MainTableForm); diff --git a/front/odiparpack/app/components/Tables/tableParts/Row.js b/front/odiparpack/app/components/Tables/tableParts/Row.js new file mode 100644 index 0000000..67e7a4d --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/Row.js @@ -0,0 +1,167 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import DeleteIcon from '@material-ui/icons/Delete'; +import EditIcon from '@material-ui/icons/BorderColor'; +import DoneIcon from '@material-ui/icons/Done'; +import css from 'ba-styles/Table.scss'; +import { TableCell, IconButton } from '@material-ui/core'; +import EditableCell from './EditableCell'; +import SelectableCell from './SelectableCell'; +import ToggleCell from './ToggleCell'; +import DatePickerCell from './DatePickerCell'; +import TimePickerCell from './TimePickerCell'; + + +const styles = theme => ({ + button: { + margin: theme.spacing(1), + }, +}); + +class Row extends React.Component { + render() { + const { + classes, + anchor, + item, + removeRow, + updateRow, + editRow, + finishEditRow, + branch + } = this.props; + const eventDel = () => { + removeRow(item, branch); + }; + const eventEdit = () => { + editRow(item, branch); + }; + const eventDone = () => { + finishEditRow(item, branch); + }; + const renderCell = dataArray => dataArray.map((itemCell, index) => { + if (itemCell.name !== 'action' && !itemCell.hidden) { + const inputType = anchor[index].type; + switch (inputType) { + case 'selection': + return ( + <SelectableCell + updateRow={(event) => updateRow(event, branch)} + cellData={{ + type: itemCell.name, + value: item.get(itemCell.name), + id: item.get('id'), + }} + edited={item.get('edited')} + key={index.toString()} + options={anchor[index].options} + branch={branch} + /> + ); + case 'toggle': + return ( + <ToggleCell + updateRow={(event) => updateRow(event, branch)} + cellData={{ + type: itemCell.name, + value: item.get(itemCell.name), + id: item.get('id'), + }} + edited={item.get('edited')} + key={index.toString()} + branch={branch} + /> + ); + case 'date': + return ( + <DatePickerCell + updateRow={(event) => updateRow(event, branch)} + cellData={{ + type: itemCell.name, + value: item.get(itemCell.name), + id: item.get('id'), + }} + edited={item.get('edited')} + key={index.toString()} + branch={branch} + /> + ); + case 'time': + return ( + <TimePickerCell + updateRow={(event) => updateRow(event, branch)} + cellData={{ + type: itemCell.name, + value: item.get(itemCell.name), + id: item.get('id'), + }} + edited={item.get('edited')} + key={index.toString()} + branch={branch} + /> + ); + default: + return ( + <EditableCell + updateRow={(event) => updateRow(event, branch)} + cellData={{ + type: itemCell.name, + value: item.get(itemCell.name), + id: item.get('id'), + }} + edited={item.get('edited')} + key={index.toString()} + inputType={inputType} + branch={branch} + /> + ); + } + } + return false; + }); + return ( + <tr className={item.get('edited') ? css.editing : ''}> + {renderCell(anchor)} + <TableCell padding="none"> + <IconButton + onClick={() => eventEdit(this)} + className={classNames((item.get('edited') ? css.hideAction : ''), classes.button)} + aria-label="Edit" + > + <EditIcon /> + </IconButton> + <IconButton + onClick={() => eventDone(this)} + color="secondary" + className={classNames((!item.get('edited') ? css.hideAction : ''), classes.button)} + aria-label="Done" + > + <DoneIcon /> + </IconButton> + <IconButton + onClick={() => eventDel(this)} + className={classes.button} + aria-label="Delete" + > + <DeleteIcon /> + </IconButton> + </TableCell> + </tr> + ); + } +} + +Row.propTypes = { + classes: PropTypes.object.isRequired, + anchor: PropTypes.array.isRequired, + item: PropTypes.object.isRequired, + removeRow: PropTypes.func.isRequired, + updateRow: PropTypes.func.isRequired, + editRow: PropTypes.func.isRequired, + finishEditRow: PropTypes.func.isRequired, + branch: PropTypes.string.isRequired +}; + +export default withStyles(styles)(Row); diff --git a/front/odiparpack/app/components/Tables/tableParts/RowReadOnly.js b/front/odiparpack/app/components/Tables/tableParts/RowReadOnly.js new file mode 100644 index 0000000..7da655f --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/RowReadOnly.js @@ -0,0 +1,76 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import css from 'ba-styles/Table.scss'; +import DeleteIcon from '@material-ui/icons/Delete'; +import EditIcon from '@material-ui/icons/BorderColor'; + +import { TableCell, IconButton } from '@material-ui/core'; + +const styles = theme => ({ + button: { + margin: theme.spacing(1), + }, +}); + +class RowReadOnly extends React.Component { + render() { + const { + anchor, + classes, + item, + removeRow, + editRow, + branch + } = this.props; + const eventDel = () => { + removeRow(item, branch); + }; + const eventEdit = () => { + editRow(item, branch); + }; + const renderCell = dataArray => dataArray.map((itemCell, index) => { + if (itemCell.name !== 'action' && !itemCell.hidden) { + return ( + <TableCell padding="none" key={index.toString()}> + {item.get(itemCell.name) !== undefined ? item.get(itemCell.name).toString() : ''} + </TableCell> + ); + } + return false; + }); + return ( + <tr> + {renderCell(anchor)} + <TableCell padding="none"> + <IconButton + onClick={() => eventEdit(this)} + className={classNames((item.get('edited') ? css.hideAction : ''), classes.button)} + aria-label="Edit" + > + <EditIcon /> + </IconButton> + <IconButton + onClick={() => eventDel(this)} + className={classes.button} + aria-label="Delete" + > + <DeleteIcon /> + </IconButton> + </TableCell> + </tr> + ); + } +} + +RowReadOnly.propTypes = { + anchor: PropTypes.array.isRequired, + classes: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, + removeRow: PropTypes.func.isRequired, + editRow: PropTypes.func.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default withStyles(styles)(RowReadOnly); diff --git a/front/odiparpack/app/components/Tables/tableParts/SelectableCell.js b/front/odiparpack/app/components/Tables/tableParts/SelectableCell.js new file mode 100644 index 0000000..66fde97 --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/SelectableCell.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import css from 'ba-styles/Table.scss'; + +import { Select, MenuItem, TableCell } from '@material-ui/core'; + +class SelectableCell extends React.Component { + handleChange = event => { + this.props.updateRow(event, this.props.branch); + this.setState({ [event.target.name]: event.target.value }); + }; + + render() { + const { + cellData, + edited, + options, + } = this.props; + return ( + <TableCell padding="none"> + <Select + name={cellData.type} + id={cellData.id.toString()} + className={css.crudInput} + value={cellData.value} + onChange={this.handleChange} + displayEmpty + disabled={!edited} + margin="none" + > + {options.map((option, index) => <MenuItem value={option} key={index.toString()}>{option}</MenuItem>)} + </Select> + </TableCell> + ); + } +} + +SelectableCell.propTypes = { + options: PropTypes.array.isRequired, + cellData: PropTypes.object.isRequired, + updateRow: PropTypes.func.isRequired, + edited: PropTypes.bool.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default SelectableCell; diff --git a/front/odiparpack/app/components/Tables/tableParts/TableHeader.js b/front/odiparpack/app/components/Tables/tableParts/TableHeader.js new file mode 100644 index 0000000..4ef6b0d --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/TableHeader.js @@ -0,0 +1,74 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TableCell, TableHead, TableRow, TableSortLabel, Checkbox, Tooltip } from '@material-ui/core'; + +class TableHeader extends React.Component { + createSortHandler = property => event => { + this.props.onRequestSort(event, property); + }; + + render() { + const { + onSelectAllClick, + order, + orderBy, + numSelected, + rowCount, + columnData, + checkcell + } = this.props; + + return ( + <TableHead> + <TableRow> + {checkcell + && ( + <TableCell padding="checkbox" width="80"> + <Checkbox + indeterminate={numSelected > 0 && numSelected < rowCount} + checked={numSelected === rowCount} + onChange={onSelectAllClick} + /> + </TableCell> + ) + } + {columnData.map(column => ( + <TableCell + key={column.id} + align={column.numeric ? 'right' : 'left'} + padding={column.disablePadding ? 'none' : 'default'} + sortDirection={orderBy === column.id ? order : false} + > + <Tooltip + title="Sort" + placement={column.numeric ? 'bottom-end' : 'bottom-start'} + enterDelay={300} + > + <TableSortLabel + active={orderBy === column.id} + direction={order} + onClick={this.createSortHandler(column.id)} + > + {column.label} + </TableSortLabel> + </Tooltip> + </TableCell> + ), this)} + </TableRow> + </TableHead> + ); + } +} + +TableHeader.propTypes = { + numSelected: PropTypes.number.isRequired, + onRequestSort: PropTypes.func.isRequired, + onSelectAllClick: PropTypes.func.isRequired, + order: PropTypes.string.isRequired, + orderBy: PropTypes.string.isRequired, + rowCount: PropTypes.number.isRequired, + columnData: PropTypes.array.isRequired, + checkcell: PropTypes.bool.isRequired, +}; + +export default TableHeader; diff --git a/front/odiparpack/app/components/Tables/tableParts/TableToolbar.js b/front/odiparpack/app/components/Tables/tableParts/TableToolbar.js new file mode 100644 index 0000000..940a82c --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/TableToolbar.js @@ -0,0 +1,123 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import DeleteIcon from '@material-ui/icons/Delete'; +import ArchiveIcon from '@material-ui/icons/Archive'; +import BookmarkIcon from '@material-ui/icons/Bookmark'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import SearchIcon from '@material-ui/icons/Search'; +import { + Toolbar, + Typography, + IconButton, + Tooltip, + FormControl, + Input, + InputAdornment, +} from '@material-ui/core'; +import styles from './tableStyle-jss'; + + +class TableToolbar extends React.Component { + state = { + showSearch: false, + } + + toggleSearch() { + this.setState({ showSearch: !this.state.showSearch }); + } + + handleChange(event) { + event.persist(); + this.props.onUserInput(event.target.value); + } + + render() { + const { numSelected, classes, filterText } = this.props; + const { showSearch } = this.state; + + return ( + <Toolbar + className={classNames(classes.root, { + [classes.highlight]: numSelected > 0, + })} + > + <div className={classes.titleToolbar}> + {numSelected > 0 ? ( + <Typography color="inherit" variant="subtitle1"> + {numSelected} + {' '} +selected + </Typography> + ) : ( + <Typography variant="h6">Nutrition</Typography> + )} + </div> + <div className={classes.spacer} /> + <div className={classes.actionsToolbar}> + {numSelected > 0 ? ( + <div> + <Tooltip title="Bookmark"> + <IconButton aria-label="Bookmark"> + <BookmarkIcon /> + </IconButton> + </Tooltip> + <Tooltip title="Archive"> + <IconButton aria-label="Archive"> + <ArchiveIcon /> + </IconButton> + </Tooltip> + <Tooltip title="Delete"> + <IconButton aria-label="Delete"> + <DeleteIcon /> + </IconButton> + </Tooltip> + </div> + ) : ( + <div className={classes.actions}> + {showSearch + && ( + <FormControl className={classNames(classes.textField)}> + <Input + id="search_filter" + type="text" + placeholder="Search Desert" + value={filterText} + onChange={(event) => this.handleChange(event)} + endAdornment={( + <InputAdornment position="end"> + <IconButton aria-label="Search filter"> + <SearchIcon /> + </IconButton> + </InputAdornment> + )} + /> + </FormControl> + ) + } + <Tooltip title="Filter list"> + <IconButton + aria-label="Filter list" + className={classes.filterBtn} + onClick={() => this.toggleSearch()} + > + <FilterListIcon /> + </IconButton> + </Tooltip> + </div> + )} + </div> + </Toolbar> + ); + } +} + +TableToolbar.propTypes = { + classes: PropTypes.object.isRequired, + filterText: PropTypes.string.isRequired, + onUserInput: PropTypes.func.isRequired, + numSelected: PropTypes.number.isRequired, +}; + +export default withStyles(styles)(TableToolbar); diff --git a/front/odiparpack/app/components/Tables/tableParts/TimePickerCell.js b/front/odiparpack/app/components/Tables/tableParts/TimePickerCell.js new file mode 100644 index 0000000..941df31 --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/TimePickerCell.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import MomentUtils from '@date-io/moment'; +import css from 'ba-styles/Table.scss'; + +import { TableCell, InputAdornment, Icon, IconButton } from '@material-ui/core'; + +class TimePickerCell extends React.Component { + state = { + event: { + target: { + name: this.props.cellData.type, // eslint-disable-line + value: this.props.cellData.value, // eslint-disable-line + } + } + } + + handleTimeChange = date => { + const { event } = this.state; + const { updateRow, branch } = this.props; + event.target.value = date; + updateRow(event, branch); + } + + render() { + const { + edited, + cellData + } = this.props; + const { event } = this.state; + return ( + <TableCell padding="none"> + <MuiPickersUtilsProvider utils={MomentUtils}> + <TimePicker + name={cellData.type} + className={css.crudInput} + mask={[/\d/, /\d/, ':', /\d/, /\d/, ' ', /a|p/i, 'M']} + placeholder="08:00 AM" + value={event.target.value} + disabled={!edited} + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <IconButton> + <Icon>access_time</Icon> + </IconButton> + </InputAdornment> + ), + }} + onChange={this.handleTimeChange} + /> + </MuiPickersUtilsProvider> + </TableCell> + ); + } +} + +TimePickerCell.propTypes = { + cellData: PropTypes.object.isRequired, + updateRow: PropTypes.func.isRequired, + edited: PropTypes.bool.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default TimePickerCell; diff --git a/front/odiparpack/app/components/Tables/tableParts/ToggleCell.js b/front/odiparpack/app/components/Tables/tableParts/ToggleCell.js new file mode 100644 index 0000000..dc0af89 --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/ToggleCell.js @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import css from 'ba-styles/Table.scss'; + +import { TableCell, FormControlLabel, Switch } from '@material-ui/core'; + +class ToggleCell extends React.Component { + state = { + isChecked: this.props.cellData.value + }; + + handleChange = event => { + this.setState({ isChecked: event.target.checked }); + this.props.updateRow(event, this.props.branch); + }; + + render() { + const { + cellData, + edited, + } = this.props; + return ( + <TableCell className={css.toggleCell} padding="none" textalign="center"> + <div className={classNames(css.coverReadonly, !edited ? css.show : '')} /> + <FormControlLabel + control={( + <Switch + name={cellData.type} + id={cellData.id.toString()} + className={css.crudInput} + checked={this.state.isChecked} + onChange={this.handleChange} + value={cellData.value.toString()} + /> + )} + /> + </TableCell> + ); + } +} + +ToggleCell.propTypes = { + cellData: PropTypes.object.isRequired, + updateRow: PropTypes.func.isRequired, + edited: PropTypes.bool.isRequired, + branch: PropTypes.string.isRequired, +}; + +export default ToggleCell; diff --git a/front/odiparpack/app/components/Tables/tableParts/tableStyle-jss.js b/front/odiparpack/app/components/Tables/tableParts/tableStyle-jss.js new file mode 100644 index 0000000..bede0b8 --- /dev/null +++ b/front/odiparpack/app/components/Tables/tableParts/tableStyle-jss.js @@ -0,0 +1,63 @@ +import { lighten } from '@material-ui/core/styles/colorManipulator'; +const styles = theme => ({ + root: { + paddingRight: theme.spacing(1), + }, + rootTable: { + width: '100%', + marginTop: theme.spacing(3), + overflowX: 'auto', + }, + highlight: + theme.palette.type === 'light' ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + spacer: { + flex: '1 1 100%', + }, + actionsToolbar: { + color: theme.palette.text.secondary, + flex: '1 0 auto', + }, + titleToolbar: { + flex: '0 0 auto', + }, + filterBtn: { + top: -5, + }, + textField: { + flexBasis: 200, + width: 300 + }, + table: { + minWidth: 900, + }, + actions: { + color: theme.palette.text.secondary, + margin: 10 + }, + toolbar: { + backgroundColor: theme.palette.grey[800], + }, + title: { + flex: '0 0 auto', + '& h6': { + color: theme.palette.common.white + } + }, + button: { + margin: theme.spacing(1), + }, + iconSmall: { + fontSize: 20, + }, + leftIcon: { + marginRight: theme.spacing(1), + }, +}); + +export default styles; diff --git a/front/odiparpack/app/components/TemplateSettings/ThemeThumb.js b/front/odiparpack/app/components/TemplateSettings/ThemeThumb.js new file mode 100644 index 0000000..df16917 --- /dev/null +++ b/front/odiparpack/app/components/TemplateSettings/ThemeThumb.js @@ -0,0 +1,74 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/core/styles'; +import themePalette from 'ba-api/themePalette'; +import { Radio, Paper } from '@material-ui/core'; +import styles from './themeStyles-jss'; + +const ThemeThumb = props => { + const { classes } = props; + return ( + <Paper className={classNames(classes.thumb, props.theme === props.value ? classes.selectedTheme : '')}> + <Radio + checked={props.selectedValue === props.value} + value={props.value} + onChange={props.handleChange} + /> + { props.name } + <div className={classes.appPreview}> + <header style={{ background: themePalette(props.value).palette.primary.main }} /> + <aside> + <ul> + {[0, 1, 2, 3].map(index => { + if (index === 1) { + return ( + <li key={index.toString()}> + <i style={{ background: themePalette(props.value).palette.secondary.main }} /> + <p style={{ background: themePalette(props.value).palette.secondary.main }} /> + </li> + ); + } + return ( + <li key={index.toString()}> + <i style={{ background: themePalette(props.value).palette.secondary.main }} /> + <p /> + </li> + ); + })} + </ul> + </aside> + <Paper className={classes.content}> + <div className={classes.title} style={{ background: themePalette(props.value).palette.primary.main }} /> + <div className={classes.ctn1} style={{ background: themePalette(props.value).palette.secondary.main }} /> + <div className={classes.ctn2} style={{ background: themePalette(props.value).palette.primary.light }} /> + <div className={classes.ctn2} style={{ background: themePalette(props.value).palette.primary.light }} /> + <div className={classes.ctn2} style={{ background: themePalette(props.value).palette.secondary.light }} /> + <div className={classes.ctn2} style={{ background: themePalette(props.value).palette.secondary.light }} /> + </Paper> + </div> + </Paper> + ); +}; + +ThemeThumb.propTypes = { + value: PropTypes.string.isRequired, + theme: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + selectedValue: PropTypes.string.isRequired, + classes: PropTypes.object.isRequired, + handleChange: PropTypes.func.isRequired, +}; + +// Redux +const reducer = 'ui'; +const mapStateToProps = state => ({ + theme: state.getIn([reducer, 'theme']), +}); + +const ThumbsMapped = connect( + mapStateToProps, +)(ThemeThumb); + +export default withStyles(styles)(ThumbsMapped); diff --git a/front/odiparpack/app/components/TemplateSettings/index.js b/front/odiparpack/app/components/TemplateSettings/index.js new file mode 100644 index 0000000..d36f5ec --- /dev/null +++ b/front/odiparpack/app/components/TemplateSettings/index.js @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { + FormControl, + Grid, + FormControlLabel, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Icon, + Slide, + Button, +} from '@material-ui/core'; +import styles from './themeStyles-jss'; +import ThemeThumb from './ThemeThumb'; + +const Transition = React.forwardRef(function Transition(props, ref) { // eslint-disable-line + return <Slide direction="left" ref={ref} {...props} />; +}); + +function TemplateSettings(props) { + const { + classes, + palette, + open, + selectedValue, + changeTheme, + close + } = props; + + const getItem = dataArray => dataArray.map((item, index) => ( + <FormControlLabel + key={index.toString()} + className={classes.themeField} + control={( + <ThemeThumb + value={item.value} + selectedValue={selectedValue} + handleChange={changeTheme} + name={item.name} + /> + )} + /> + )); + + return ( + <Dialog + open={open} + TransitionComponent={Transition} + keepMounted + onClose={close} + className={classes.themeChooser} + aria-labelledby="alert-dialog-slide-title" + aria-describedby="alert-dialog-slide-description" + maxWidth="md" + > + <DialogTitle id="alert-dialog-slide-title"> + <Icon>palette</Icon> + {' '} + Choose Theme + </DialogTitle> + <DialogContent> + <Grid container> + <FormControl component="fieldset" className={classes.group}> + { palette !== undefined && getItem(palette) } + </FormControl> + </Grid> + </DialogContent> + <DialogActions> + <Button onClick={close} color="primary"> + Done + </Button> + </DialogActions> + </Dialog> + ); +} + +TemplateSettings.propTypes = { + classes: PropTypes.object.isRequired, + palette: PropTypes.object, + selectedValue: PropTypes.string.isRequired, + changeTheme: PropTypes.func.isRequired, + close: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, +}; + +TemplateSettings.defaultProps = { + palette: undefined +}; + +export default withStyles(styles)(TemplateSettings); diff --git a/front/odiparpack/app/components/TemplateSettings/themeStyles-jss.js b/front/odiparpack/app/components/TemplateSettings/themeStyles-jss.js new file mode 100644 index 0000000..efbac9c --- /dev/null +++ b/front/odiparpack/app/components/TemplateSettings/themeStyles-jss.js @@ -0,0 +1,106 @@ +const styles = theme => ({ + group: { + margin: `${theme.spacing(1)}px 0`, + maxWidth: 1000, + display: 'block', + '& label': { + display: 'inline-block', + margin: 0, + width: '99%', + [theme.breakpoints.up('sm')]: { + width: '45%' + }, + [theme.breakpoints.up('md')]: { + width: '33.33%' + }, + }, + '& > label': { + position: 'relative', + '& > span': { + marginTop: 10, + display: 'block', + textAlign: 'center', + position: 'absolute', + top: 12, + left: 48, + } + } + }, + thumb: { + margin: theme.spacing(1) + }, + selectedTheme: { + boxShadow: `0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12), 0 0 0px 4px ${theme.palette.secondary.main}` + }, + content: {}, + title: {}, + ctn1: {}, + ctn2: {}, + appPreview: { + width: '100%', + padding: 5, + height: 200, + position: 'relative', + display: 'flex', + background: theme.palette.grey[50], + '& header': { + width: '100%', + height: 50, + background: theme.palette.primary.main, + position: 'absolute', + top: 0, + left: 0 + }, + '& aside': { + width: '30%', + marginTop: 70, + '& li': { + margin: '0 10px 10px 5px', + display: 'flex', + '& i': { + borderRadius: '50%', + width: 8, + height: 8, + marginRight: 5, + marginTop: -3, + background: theme.palette.secondary.main, + }, + '& p': { + width: 40, + height: 2, + background: theme.palette.grey[400], + } + } + }, + '& $content': { + padding: 10, + marginTop: 20, + width: '70%', + zIndex: 10, + marginBottom: 10, + '& $title': { + background: theme.palette.primary.main, + height: 5, + width: '60%', + marginBottom: 10 + }, + '& $ctn1': { + margin: '5px 5px 10px 0', + width: '100%', + height: 30, + background: theme.palette.secondary.main, + display: 'block' + }, + '& $ctn2': { + width: '50%', + marginLeft: 0, + height: 40, + border: '2px solid #FFF', + background: theme.palette.secondary.light, + display: 'inline-block' + } + } + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/Widget/AlbumWidget.js b/front/odiparpack/app/components/Widget/AlbumWidget.js new file mode 100644 index 0000000..d691361 --- /dev/null +++ b/front/odiparpack/app/components/Widget/AlbumWidget.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import InfoIcon from '@material-ui/icons/Info'; +import imgData from 'ba-api/imgData'; +import { GridList, GridListTile, GridListTileBar, IconButton } from '@material-ui/core'; +import PapperBlock from '../PapperBlock/PapperBlock'; +import styles from './widget-jss'; + + +class AlbumWidget extends React.Component { + render() { + const { classes } = this.props; + return ( + <PapperBlock noMargin title="My Albums (4)" whiteBg desc=""> + <div className={classes.albumRoot}> + <GridList cellHeight={180} className={classes.gridList}> + { + imgData.map((tile, index) => { + if (index >= 4) { + return false; + } + return ( + <GridListTile key={index.toString()}> + <img src={tile.img} className={classes.img} alt={tile.title} /> + <GridListTileBar + title={tile.title} + subtitle={( + <span> +by: + {tile.author} + </span> + )} + actionIcon={( + <IconButton className={classes.icon}> + <InfoIcon /> + </IconButton> + )} + /> + </GridListTile> + ); + }) + } + </GridList> + </div> + </PapperBlock> + ); + } +} + +AlbumWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(AlbumWidget); diff --git a/front/odiparpack/app/components/Widget/AreaChartWidget.js b/front/odiparpack/app/components/Widget/AreaChartWidget.js new file mode 100644 index 0000000..8e17311 --- /dev/null +++ b/front/odiparpack/app/components/Widget/AreaChartWidget.js @@ -0,0 +1,147 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles, createMuiTheme } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import CardGiftcard from '@material-ui/icons/CardGiftcard'; +import FilterVintage from '@material-ui/icons/FilterVintage'; +import LocalCafe from '@material-ui/icons/LocalCafe'; +import Style from '@material-ui/icons/Style'; +import themePallete from 'ba-api/themePalette'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer +} from 'recharts'; +import messageStyles from 'ba-styles/Messages.scss'; +import { data1 } from 'ba-api/chartData'; +import Type from 'ba-styles/Typography.scss'; +import { purple } from '@material-ui/core/colors'; +import { Grid, Chip, Avatar, Divider, CircularProgress, Typography } from '@material-ui/core'; +import styles from './widget-jss'; +import PapperBlock from '../PapperBlock/PapperBlock'; + + +const theme = createMuiTheme(themePallete('magentaTheme')); +const color = ({ + primary: theme.palette.primary.main, + primarydark: theme.palette.primary.dark, + secondary: theme.palette.secondary.main, + secondarydark: theme.palette.secondary.dark, + third: purple[500], + thirddark: purple[900], +}); + +class AreaChartWidget extends PureComponent { + render() { + const { + classes, + } = this.props; + return ( + <PapperBlock whiteBg noMargin title="Top Product Sales" desc=""> + <Grid container spacing={2}> + <Grid item md={8} xs={12}> + <ul className={classes.bigResume}> + <li> + <Avatar className={classNames(classes.avatar, classes.pinkAvatar)}> + <CardGiftcard /> + </Avatar> + <Typography variant="h6"> + 4321 + <Typography>Gift Card</Typography> + </Typography> + </li> + <li> + <Avatar className={classNames(classes.avatar, classes.purpleAvatar)}> + <FilterVintage /> + </Avatar> + <Typography variant="h6"> + 9876 + <Typography>Flowers</Typography> + </Typography> + </li> + <li> + <Avatar className={classNames(classes.avatar, classes.blueAvatar)}> + <LocalCafe /> + </Avatar> + <Typography variant="h6"> + 345 + <Typography>Cups</Typography> + </Typography> + </li> + <li> + <Avatar className={classNames(classes.avatar, classes.tealAvatar)}> + <Style /> + </Avatar> + <Typography variant="h6"> + 996 + <Typography>Name Cards</Typography> + </Typography> + </li> + </ul> + <div className={classes.chartWrap}> + <div className={classes.chartFluid}> + <ResponsiveContainer> + <AreaChart + width={600} + height={300} + data={data1} + margin={{ + top: 5, + right: 30, + left: 20, + bottom: 5 + }} + > + <XAxis dataKey="name" /> + <YAxis /> + <CartesianGrid strokeDasharray="3 3" /> + <Tooltip /> + <Area type="monotone" dataKey="uv" stackId="1" stroke={color.primarydark} fillOpacity="0.8" fill={color.primary} /> + <Area type="monotone" dataKey="pv" stackId="1" stroke={color.secondarydark} fillOpacity="0.8" fill={color.secondary} /> + <Area type="monotone" dataKey="amt" stackId="1" stroke={color.thirddark} fillOpacity="0.8" fill={color.third} /> + </AreaChart> + </ResponsiveContainer> + </div> + </div> + </Grid> + <Grid item md={4} xs={12}> + <Typography variant="button"><span className={Type.bold}>Performance Listing</span></Typography> + <Divider className={classes.divider} /> + <Grid container className={classes.secondaryWrap}> + <Grid item className={classes.centerItem} md={6}> + <Typography gutterBottom>Giftcard Quality</Typography> + <Chip label="Super" className={classNames(classes.chip, messageStyles.bgError)} /> + <CircularProgress variant="determinate" className={classNames(classes.progressCircle, classes.pinkProgress)} size={100} value={70} /> + </Grid> + <Grid className={classes.centerItem} item md={6}> + <Typography gutterBottom>Monitoring Quality</Typography> + <Chip label="Good" className={classNames(classes.chip, messageStyles.bgSuccess)} /> + <CircularProgress variant="determinate" className={classNames(classes.progressCircle, classes.greenProgress)} size={100} value={57} /> + </Grid> + <Grid className={classes.centerItem} item md={6}> + <Typography gutterBottom>Project Complete</Typography> + <Chip label="Poor" className={classNames(classes.chip, messageStyles.bgWarning)} /> + <CircularProgress variant="determinate" className={classNames(classes.progressCircle, classes.orangeProgress)} size={100} value={28} /> + </Grid> + <Grid className={classes.centerItem} item md={6}> + <Typography gutterBottom>Deploy Progress</Typography> + <Chip label="Medium" className={classNames(classes.chip, messageStyles.bgInfo)} /> + <CircularProgress variant="determinate" className={classNames(classes.progressCircle, classes.blueProgress)} size={100} value={70} /> + </Grid> + </Grid> + </Grid> + </Grid> + </PapperBlock> + ); + } +} + +AreaChartWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(AreaChartWidget); diff --git a/front/odiparpack/app/components/Widget/BigChartWidget.js b/front/odiparpack/app/components/Widget/BigChartWidget.js new file mode 100644 index 0000000..718af45 --- /dev/null +++ b/front/odiparpack/app/components/Widget/BigChartWidget.js @@ -0,0 +1,142 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import Dvr from '@material-ui/icons/Dvr'; +import Explore from '@material-ui/icons/Explore'; +import Healing from '@material-ui/icons/Healing'; +import LocalActivity from '@material-ui/icons/LocalActivity'; +import { + ComposedChart, + Line, + Area, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer +} from 'recharts'; +import { data1 } from 'ba-api/chartData'; +import Type from 'ba-styles/Typography.scss'; +import colorfull from 'ba-api/colorfull'; +import { Grid, Avatar, Divider, LinearProgress, Typography } from '@material-ui/core'; +import styles from './widget-jss'; +import PapperBlock from '../PapperBlock/PapperBlock'; + + +const color = ({ + main: colorfull[5], + maindark: '#2196F3', + secondary: colorfull[3], + third: colorfull[0], +}); + +class BigChartWidget extends PureComponent { + render() { + const { + classes, + } = this.props; + return ( + <PapperBlock whiteBg noMargin title="Top Product Sales" desc=""> + <Grid container spacing={2}> + <Grid item md={8} xs={12}> + <ul className={classes.bigResume}> + <li> + <Avatar className={classNames(classes.avatar, classes.pinkAvatar)}> + <Dvr /> + </Avatar> + <Typography variant="h6"> + 1234 + <Typography>Monitors</Typography> + </Typography> + </li> + <li> + <Avatar className={classNames(classes.avatar, classes.purpleAvatar)}> + <Explore /> + </Avatar> + <Typography variant="h6"> + 5678 + <Typography>Compas</Typography> + </Typography> + </li> + <li> + <Avatar className={classNames(classes.avatar, classes.blueAvatar)}> + <Healing /> + </Avatar> + <Typography variant="h6"> + 910 + <Typography>Badges</Typography> + </Typography> + </li> + <li> + <Avatar className={classNames(classes.avatar, classes.tealAvatar)}> + <LocalActivity /> + </Avatar> + <Typography variant="h6"> + 1112 + <Typography>Tickets</Typography> + </Typography> + </li> + </ul> + <div className={classes.chartWrap}> + <div className={classes.chartFluid}> + <ResponsiveContainer> + <ComposedChart + data={data1} + margin={{ + top: 0, + right: 30, + left: 20, + bottom: 20 + }} + > + <CartesianGrid stroke="#f5f5f5" /> + <XAxis dataKey="name" /> + <YAxis /> + <Tooltip /> + <Area type="monotone" dataKey="amt" fill={color.main} stroke={color.maindark} /> + <Bar dataKey="pv" barSize={20} fill={color.secondary} /> + <Line type="monotone" dataKey="uv" stroke={color.third} /> + </ComposedChart> + </ResponsiveContainer> + </div> + </div> + </Grid> + <Grid item md={4} xs={12}> + <Typography variant="button"><span className={Type.bold}>Performance Listing</span></Typography> + <Divider className={classes.divider} /> + <ul className={classes.secondaryWrap}> + <li> + <Typography gutterBottom>Monitoring Quality</Typography> + <LinearProgress variant="determinate" className={classNames(classes.progress, classes.pinkProgress)} value={24} /> + </li> + <li> + <Typography gutterBottom>Compas Speed</Typography> + <LinearProgress variant="determinate" className={classNames(classes.progress, classes.purpleProgress)} value={89} /> + </li> + <li> + <Typography gutterBottom>Total Badges</Typography> + <LinearProgress variant="determinate" className={classNames(classes.progress, classes.orangeProgress)} value={78} /> + </li> + <li> + <Typography gutterBottom>Sold Ticket</Typography> + <LinearProgress variant="determinate" className={classNames(classes.progress, classes.greenProgress)} value={55} /> + </li> + <li> + <Typography gutterBottom>App Performance</Typography> + <LinearProgress variant="determinate" className={classNames(classes.progress, classes.blueProgress)} value={80} /> + </li> + </ul> + </Grid> + </Grid> + </PapperBlock> + ); + } +} + +BigChartWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(BigChartWidget); diff --git a/front/odiparpack/app/components/Widget/CarouselWidget.js b/front/odiparpack/app/components/Widget/CarouselWidget.js new file mode 100644 index 0000000..51dd522 --- /dev/null +++ b/front/odiparpack/app/components/Widget/CarouselWidget.js @@ -0,0 +1,117 @@ +import React from 'react'; +import Slider from 'react-slick'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import ArrowForward from '@material-ui/icons/ArrowForward'; +import ArrowBack from '@material-ui/icons/ArrowBack'; +import carouselData from 'ba-api/carouselData'; +import 'ba-styles/vendors/slick-carousel/slick-carousel.css'; +import 'ba-styles/vendors/slick-carousel/slick.css'; +import 'ba-styles/vendors/slick-carousel/slick-theme.css'; +import { Typography, IconButton, Icon } from '@material-ui/core'; +import styles from './widget-jss'; + + +function SampleNextArrow(props) { + const { onClick } = props; + return ( + <IconButton + className="nav-next" + onClick={onClick} + > + <ArrowForward /> + </IconButton> + ); +} + +SampleNextArrow.propTypes = { + onClick: PropTypes.func, +}; + +SampleNextArrow.defaultProps = { + onClick: undefined, +}; + +function SamplePrevArrow(props) { + const { onClick } = props; + return ( + <IconButton + className="nav-prev" + onClick={onClick} + > + <ArrowBack /> + </IconButton> + ); +} + +SamplePrevArrow.propTypes = { + onClick: PropTypes.func, +}; + +SamplePrevArrow.defaultProps = { + onClick: undefined, +}; + +class CarouselWidget extends React.Component { + render() { + const { classes } = this.props; + const settings = { + dots: true, + infinite: true, + centerMode: false, + speed: 500, + autoplaySpeed: 5000, + pauseOnHover: true, + autoplay: true, + slidesToShow: 3, + slidesToScroll: 1, + responsive: [ + { + breakpoint: 960, + settings: { + slidesToShow: 2, + slidesToScroll: 1, + infinite: true, + dots: true + } + }, + { + breakpoint: 600, + settings: { + slidesToShow: 1, + slidesToScroll: 1, + infinite: true, + dots: true + } + }, + ], + cssEase: 'ease-out', + nextArrow: <SampleNextArrow />, + prevArrow: <SamplePrevArrow /> + }; + return ( + <div className="container custom-arrow"> + <Slider {...settings}> + {carouselData.map((item, index) => ( + <div key={index.toString()}> + <div style={{ backgroundColor: item.background }} className={classes.carouselItem}> + <Icon className={classes.iconBg}>{item.icon}</Icon> + <Typography className={classes.carouselTitle} variant="subtitle1"> + <Icon>{item.icon}</Icon> + {item.title} + </Typography> + <Typography className={classes.carouselDesc}>{item.desc}</Typography> + </div> + </div> + ))} + </Slider> + </div> + ); + } +} + +CarouselWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(CarouselWidget); diff --git a/front/odiparpack/app/components/Widget/CounterGroupWidget.js b/front/odiparpack/app/components/Widget/CounterGroupWidget.js new file mode 100644 index 0000000..92856b8 --- /dev/null +++ b/front/odiparpack/app/components/Widget/CounterGroupWidget.js @@ -0,0 +1,101 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { + BarChart, Bar, + AreaChart, Area, + LineChart, Line, + PieChart, Pie, Cell +} from 'recharts'; +import { data1, data2 } from 'ba-api/chartMiniData'; +import colorfull from 'ba-api/colorfull'; +import { red, blue, cyan, lime } from '@material-ui/core/colors'; +import { Grid } from '@material-ui/core'; +import CounterWidget from '../Counter/CounterWidget'; +import styles from './widget-jss'; + + +const colors = [red[300], blue[300], cyan[300], lime[300]]; + +class CounterGroupWidget extends PureComponent { + render() { + const { classes } = this.props; + return ( + <div className={classes.rootCounter}> + <Grid container spacing={3}> + <Grid item xs={6}> + <CounterWidget + color={colorfull[0]} + start={0} + end={105} + duration={3} + title="New Customers" + > + <BarChart width={100} height={40} data={data1}> + <Bar dataKey="uv" fill="#ffffff" /> + </BarChart> + </CounterWidget> + </Grid> + <Grid item xs={6}> + <CounterWidget + color={colorfull[1]} + start={0} + end={321} + duration={3} + title="New Articles" + > + <AreaChart width={100} height={60} data={data1}> + <Area type="monotone" dataKey="uv" stroke="#FFFFFF" fill="rgba(255,255,255,.5)" /> + </AreaChart> + </CounterWidget> + </Grid> + <Grid item xs={6}> + <CounterWidget + color={colorfull[2]} + start={0} + end={67} + duration={3} + title="New Contributor" + > + <LineChart width={100} height={80} data={data1}> + <Line type="monotone" dataKey="pv" stroke="#FFFFFF" strokeWidth={2} /> + </LineChart> + </CounterWidget> + </Grid> + <Grid item xs={6}> + <CounterWidget + color={colorfull[3]} + start={0} + end={80} + duration={3} + title="Average Income" + > + <PieChart width={100} height={100}> + <Pie + data={data2} + cx={50} + cy={50} + dataKey="value" + innerRadius={20} + outerRadius={40} + fill="#FFFFFF" + paddingAngle={5} + > + { + data2.map((entry, index) => <Cell key={index.toString()} fill={colors[index % colors.length]} />) + } + </Pie> + </PieChart> + </CounterWidget> + </Grid> + </Grid> + </div> + ); + } +} + +CounterGroupWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(CounterGroupWidget); diff --git a/front/odiparpack/app/components/Widget/CounterIconsWidget.js b/front/odiparpack/app/components/Widget/CounterIconsWidget.js new file mode 100644 index 0000000..9c10850 --- /dev/null +++ b/front/odiparpack/app/components/Widget/CounterIconsWidget.js @@ -0,0 +1,74 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import AccountBox from '@material-ui/icons/AccountBox'; +import ImportContacts from '@material-ui/icons/ImportContacts'; +import Pets from '@material-ui/icons/Pets'; +import Star from '@material-ui/icons/Star'; +import colorfull from 'ba-api/colorfull'; +import { Grid } from '@material-ui/core'; +import CounterWidget from '../Counter/CounterWidget'; +import styles from './widget-jss'; + + +class CounterIconWidget extends PureComponent { + render() { + const { classes } = this.props; + return ( + <div className={classes.rootCounterFull}> + <Grid container spacing={2}> + <Grid item md={3} xs={6}> + <CounterWidget + color={colorfull[0]} + start={0} + end={105} + duration={3} + title="New Customers" + > + <AccountBox className={classes.counterIcon} /> + </CounterWidget> + </Grid> + <Grid item md={3} xs={6}> + <CounterWidget + color={colorfull[1]} + start={0} + end={321} + duration={3} + title="New Articles" + > + <ImportContacts className={classes.counterIcon} /> + </CounterWidget> + </Grid> + <Grid item md={3} xs={6}> + <CounterWidget + color={colorfull[2]} + start={0} + end={67} + duration={3} + title="New Contributor" + > + <Pets className={classes.counterIcon} /> + </CounterWidget> + </Grid> + <Grid item md={3} xs={6}> + <CounterWidget + color={colorfull[3]} + start={0} + end={80} + duration={3} + title="Average Income" + > + <Star className={classes.counterIcon} /> + </CounterWidget> + </Grid> + </Grid> + </div> + ); + } +} + +CounterIconWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(CounterIconWidget); diff --git a/front/odiparpack/app/components/Widget/MapWidget.js b/front/odiparpack/app/components/Widget/MapWidget.js new file mode 100644 index 0000000..37204e9 --- /dev/null +++ b/front/odiparpack/app/components/Widget/MapWidget.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { compose } from 'recompose'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import dummy from 'ba-api/dummyContents'; +import { + withScriptjs, + withGoogleMap, + GoogleMap, + Marker, +} from 'react-google-maps'; +import { Paper } from '@material-ui/core'; +import IdentityCard from '../CardPaper/IdentityCard'; +import styles from './widget-jss'; + +const MapWithAMarker = compose( + withScriptjs, + withGoogleMap +)(props => ( + <GoogleMap + {...props} + defaultZoom={8} + defaultCenter={{ lat: -34.300, lng: 119.344 }} + > + <Marker + position={{ lat: -34.300, lng: 118.044 }} + /> + </GoogleMap> +)); + +class MapWidget extends React.Component { + render() { + const { classes } = this.props; + return ( + <Paper className={classes.mapWrap}> + <MapWithAMarker + googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places" + loadingElement={<div style={{ height: '100%' }} />} + containerElement={<div style={{ height: '400px' }} />} + mapElement={<div style={{ height: '100%' }} />} + /> + <div className={classes.address}> + <IdentityCard + title="Contact and Address" + name={dummy.user.name} + avatar={dummy.user.avatar} + phone="(+8543201213)" + address="Town Hall Building no.45 Block C - ABC Street" + /> + </div> + </Paper> + ); + } +} + +MapWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(MapWidget); diff --git a/front/odiparpack/app/components/Widget/ProfileWidget.js b/front/odiparpack/app/components/Widget/ProfileWidget.js new file mode 100644 index 0000000..3ae3690 --- /dev/null +++ b/front/odiparpack/app/components/Widget/ProfileWidget.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import LocalPhone from '@material-ui/icons/LocalPhone'; +import DateRange from '@material-ui/icons/DateRange'; +import LocationOn from '@material-ui/icons/LocationOn'; +import { + List, ListItem, ListItemAvatar, + ListItemText, Divider, Avatar +} from '@material-ui/core'; +import PapperBlock from '../PapperBlock/PapperBlock'; +import styles from './widget-jss'; + + +function ProfileWidget(props) { + const { classes } = props; + return ( + <PapperBlock title="About Me" whiteBg noMargin desc="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sed urna in justo euismod condimentum."> + <Divider className={classes.divider} /> + <List dense className={classes.profileList}> + <ListItem> + <ListItemAvatar> + <Avatar> + <DateRange /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Born" secondary="Jan 9, 1994" /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <LocalPhone /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Phone" secondary="(+62)8765432190" /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <LocationOn /> + </Avatar> + </ListItemAvatar> + <ListItemText primary="Address" secondary="Chicendo Street no.105 Block A/5A - Barcelona, Spain" /> + </ListItem> + </List> + </PapperBlock> + ); +} + +ProfileWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(ProfileWidget); diff --git a/front/odiparpack/app/components/Widget/ProgressWidget.js b/front/odiparpack/app/components/Widget/ProgressWidget.js new file mode 100644 index 0000000..c0e6db6 --- /dev/null +++ b/front/odiparpack/app/components/Widget/ProgressWidget.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Type from 'ba-styles/Typography.scss'; +import Check from '@material-ui/icons/Check'; +import { withStyles } from '@material-ui/core/styles'; +import { LinearProgress, Paper, Typography, Grid, Avatar, Chip } from '@material-ui/core'; +import styles from './widget-jss'; + + +function ProgressWidget(props) { + const { classes } = props; + return ( + <Paper className={classes.styledPaper} elevation={4}> + <Typography className={classes.title} variant="h5" component="h3"> + <span className={Type.light}>Profile Strength: </span> + <span className={Type.bold}>Intermediate</span> + </Typography> + <Grid container justify="center"> + <Chip + avatar={( + <Avatar> + <Check /> + </Avatar> + )} + label="60% Progress" + className={classes.chipProgress} + color="primary" + /> + </Grid> + <LinearProgress variant="determinate" className={classes.progressWidget} value={60} /> + </Paper> + ); +} + +ProgressWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(ProgressWidget); diff --git a/front/odiparpack/app/components/Widget/SliderWidget.js b/front/odiparpack/app/components/Widget/SliderWidget.js new file mode 100644 index 0000000..1f53780 --- /dev/null +++ b/front/odiparpack/app/components/Widget/SliderWidget.js @@ -0,0 +1,69 @@ +import React from 'react'; +import Type from 'ba-styles/Typography.scss'; +import Slider from 'react-animated-slider'; +import 'ba-styles/vendors/react-animated-slider/react-animated-slider.css'; +import imgApi from 'ba-api/images'; +import avatarApi from 'ba-api/avatars'; + +import { Button, Typography } from '@material-ui/core'; + +const content = [ + { + title: 'Vulputate Mollis Ultricies', + description: + 'Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum.', + button: 'Read More', + image: imgApi[3], + user: 'Luanda Gjokaj', + userProfile: avatarApi[1] + }, + { + title: 'Tortor Dapibus', + description: + 'Cras mattis consectetur purus sit amet fermentum.', + button: 'Discover', + image: imgApi[15], + user: 'Erich Behrens', + userProfile: avatarApi[8] + }, + { + title: 'Phasellus volutpat', + description: + 'Lorem ipsum dolor sit amet', + button: 'Buy now', + image: imgApi[29], + user: 'Bruno Vizovskyy', + userProfile: avatarApi[10] + } +]; + +const SliderWidget = () => ( + <div> + <Slider className="slider-wrapper short" autoplay={3000}> + {content.map((item, index) => ( + <div + key={index.toString()} + className="slider-content" + style={{ background: `url('${item.image}') no-repeat center center` }} + > + <div className="inner"> + <Typography variant="subtitle1" component="h3" className={Type.light} gutterBottom>{item.title}</Typography> + <Button variant="contained" color="primary"> + {item.button} + </Button> + </div> + <section> + <img src={item.userProfile} alt={item.user} /> + <span> + Posted by + {' '} + <strong>{item.user}</strong> + </span> + </section> + </div> + ))} + </Slider> + </div> +); + +export default SliderWidget; diff --git a/front/odiparpack/app/components/Widget/TableWidget.js b/front/odiparpack/app/components/Widget/TableWidget.js new file mode 100644 index 0000000..47bb7ba --- /dev/null +++ b/front/odiparpack/app/components/Widget/TableWidget.js @@ -0,0 +1,124 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import messageStyles from 'ba-styles/Messages.scss'; +import progressStyles from 'ba-styles/Progress.scss'; +import avatarApi from 'ba-api/avatars'; +import { + Typography, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Chip, + LinearProgress, + Avatar, + Icon, +} from '@material-ui/core'; +import PapperBlock from '../PapperBlock/PapperBlock'; +import styles from './widget-jss'; + + +let id = 0; +function createData(name, avatar, title, type, taskNumber, taskTitle, progress, status) { + id += 1; + return { + id, + name, + avatar, + title, + type, + taskNumber, + taskTitle, + progress, + status, + }; +} + +const data = [ + createData('John Doe', avatarApi[6], 'Front End Developer', 'bug_report', 2214, 'Vivamus sit amet interdum elit', 30, 'Error'), + createData('Jim Doe', avatarApi[8], 'System Analyst', 'flag', 2455, 'Nam sollicitudin dignissim nunc', 70, 'Success'), + createData('Jane Doe', avatarApi[2], 'Back End Developer', 'whatshot', 3450, 'Quisque ut metus sit amet augue rutrum', 50, 'Warning'), + createData('Jack Doe', avatarApi[9], 'CTO', 'settings', 4905, 'Cras convallis lacus orci', 85, 'Info'), + createData('Jessica Doe', avatarApi[5], 'Project Manager', 'book', 4118, 'Aenean sit amet magna vel magna', 33, 'Default'), +]; + +function TableWidget(props) { + const { classes } = props; + const getStatus = status => { + switch (status) { + case 'Error': return messageStyles.bgError; + case 'Warning': return messageStyles.bgWarning; + case 'Info': return messageStyles.bgInfo; + case 'Success': return messageStyles.bgSuccess; + default: return messageStyles.bgDefault; + } + }; + const getProgress = status => { + switch (status) { + case 'Error': return progressStyles.bgError; + case 'Warning': return progressStyles.bgWarning; + case 'Info': return progressStyles.bgInfo; + case 'Success': return progressStyles.bgSuccess; + default: return progressStyles.bgDefault; + } + }; + const getType = type => { + switch (type) { + case 'bug_report': return classes.red; + case 'flag': return classes.indigo; + case 'whatshot': return classes.orange; + case 'settings': return classes.lime; + default: return classes.purple; + } + }; + return ( + <PapperBlock noMargin title="Tracking Table" whiteBg desc="Monitoring Your Team progress. Tracking task, current progress, and task status here."> + <div className={classes.root}> + <Table className={classNames(classes.table)}> + <TableHead> + <TableRow> + <TableCell>Name</TableCell> + <TableCell>Task</TableCell> + </TableRow> + </TableHead> + <TableBody> + {data.map(n => ([ + <TableRow key={n.id}> + <TableCell> + <div className={classes.user}> + <Avatar alt={n.name} src={n.avatar} className={classes.avatar} /> + <div> + <Typography variant="subtitle1">{n.name}</Typography> + <Typography>{n.title}</Typography> + </div> + </div> + </TableCell> + <TableCell> + <div className={classes.taskStatus}> + <Icon className={classNames(classes.taskIcon, getType(n.type))}>{n.type}</Icon> + <a href="#"> + # + {n.taskNumber} + </a> + <Chip label={n.status} className={classNames(classes.chip, getStatus(n.status))} /> + </div> + <LinearProgress variant="determinate" className={getProgress(n.status)} value={n.progress} /> + </TableCell> + </TableRow> + ]) + )} + </TableBody> + </Table> + </div> + </PapperBlock> + ); +} + +TableWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(TableWidget); diff --git a/front/odiparpack/app/components/Widget/TaskWidget.js b/front/odiparpack/app/components/Widget/TaskWidget.js new file mode 100644 index 0000000..4136544 --- /dev/null +++ b/front/odiparpack/app/components/Widget/TaskWidget.js @@ -0,0 +1,83 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; +import CommentIcon from '@material-ui/icons/Comment'; +import { + List, + ListItem, + ListItemSecondaryAction, + ListItemText, + Checkbox, + IconButton, +} from '@material-ui/core'; +import PapperBlock from '../PapperBlock/PapperBlock'; +import styles from './widget-jss'; + + +class TaskWidget extends React.Component { + state = { + checked: [0], + }; + + handleToggle = value => () => { + const { checked } = this.state; + const currentIndex = checked.indexOf(value); + const newChecked = [...checked]; + + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); + } + + this.setState({ + checked: newChecked, + }); + }; + + render() { + const { classes } = this.props; + return ( + <PapperBlock title="My Task" whiteBg colorMode desc="All Your to do list. Just check it whenever You done." className={classes.root}> + <List className={classes.taskList}> + {[0, 1, 2, 3, 4].map(value => ( + <Fragment key={value}> + <ListItem + key={value} + role={undefined} + dense + button + onClick={this.handleToggle(value)} + className={ + classNames( + classes.listItem, + this.state.checked.indexOf(value) !== -1 ? classes.done : '' + ) + } + > + <Checkbox + checked={this.state.checked.indexOf(value) !== -1} + tabIndex={-1} + disableRipple + /> + <ListItemText primary={`Task item ${value + 1}`} secondary={`Task description ${value + 1}`} /> + <ListItemSecondaryAction> + <IconButton aria-label="Comments"> + <CommentIcon /> + </IconButton> + </ListItemSecondaryAction> + </ListItem> + </Fragment> + ))} + </List> + </PapperBlock> + ); + } +} + +TaskWidget.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(TaskWidget); diff --git a/front/odiparpack/app/components/Widget/widget-jss.js b/front/odiparpack/app/components/Widget/widget-jss.js new file mode 100644 index 0000000..d553eaa --- /dev/null +++ b/front/odiparpack/app/components/Widget/widget-jss.js @@ -0,0 +1,294 @@ +import colorfull from 'ba-api/colorfull'; + +const styles = theme => ({ + rootCounter: { + flexGrow: 1, + padding: theme.spacing(1.5), + [theme.breakpoints.up('lg')]: { + padding: `${theme.spacing(1.5)}px ${theme.spacing(1)}px`, + }, + [theme.breakpoints.down('xs')]: { + margin: `0 ${theme.spacing(1) * -1.5}px`, + } + }, + rootCounterFull: { + flexGrow: 1, + }, + divider: { + margin: `${theme.spacing(3)}px 0` + }, + dividerBig: { + margin: `${theme.spacing(2)}px 0` + }, + centerItem: {}, + secondaryWrap: { + background: theme.palette.grey[100], + padding: 20, + borderRadius: 4, + justifyContent: 'space-around', + '& > $centerItem': { + position: 'relative', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + '& li': { + marginBottom: 30 + }, + '& $chip': { + top: 70, + position: 'absolute', + fontSize: 11, + fontWeight: 400, + } + }, + bigResume: { + marginBottom: 20, + justifyContent: 'space-between', + display: 'flex', + [theme.breakpoints.down('sm')]: { + height: 160, + display: 'block', + }, + '& li': { + paddingRight: 20, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + [theme.breakpoints.down('sm')]: { + width: '50%', + float: 'left' + }, + }, + }, + avatar: { + width: 50, + height: 50, + marginRight: 10, + '& svg': { + fontSize: 32 + } + }, + pinkAvatar: { + margin: 10, + color: '#fff', + backgroundColor: colorfull[0], + }, + purpleAvatar: { + margin: 10, + color: '#fff', + backgroundColor: colorfull[1], + }, + blueAvatar: { + margin: 10, + color: '#fff', + backgroundColor: colorfull[2], + }, + tealAvatar: { + margin: 10, + color: '#fff', + backgroundColor: colorfull[3], + }, + pinkProgress: { + color: colorfull[0], + '& div': { + backgroundColor: colorfull[0], + } + }, + greenProgress: { + color: colorfull[5], + '& div': { + backgroundColor: colorfull[5], + } + }, + orangeProgress: { + color: colorfull[4], + '& div': { + backgroundColor: colorfull[4], + } + }, + purpleProgress: { + color: colorfull[1], + '& div': { + backgroundColor: colorfull[1], + } + }, + blueProgress: { + color: colorfull[2], + '& div': { + backgroundColor: colorfull[2], + } + }, + root: { + width: '100%', + marginTop: theme.spacing(3), + overflowX: 'auto', + }, + chip: { + margin: '8px 0 8px auto', + color: '#FFF' + }, + table: { + minWidth: 500, + '& td': { + padding: 10, + } + }, + user: { + display: 'flex', + }, + textCenter: { + textAlign: 'center' + }, + red: {}, + orange: {}, + indigo: {}, + purple: {}, + lime: {}, + taskIcon: { + display: 'block', + textAlign: 'center', + margin: '0 10px', + '&$red': { + color: colorfull[0], + }, + '&$orange': { + color: colorfull[2], + }, + '&$purple': { + color: colorfull[1], + }, + '&$lime': { + color: colorfull[3], + }, + '&$indigo': { + color: colorfull[4], + } + }, + done: {}, + listItem: { + padding: 5, + background: theme.palette.common.white, + boxShadow: theme.shadows[3], + '&:hover': { + backgroundColor: theme.palette.grey[200] + }, + '&$done': { + textDecoration: 'line-through' + } + }, + title: {}, + subtitle: {}, + styledPaper: { + backgroundColor: theme.palette.secondary.main, + padding: 20, + '& $title, & $subtitle': { + color: theme.palette.common.white + } + }, + progressWidget: { + marginTop: 20, + background: theme.palette.secondary.dark, + '& div': { + background: theme.palette.primary.light, + } + }, + chipProgress: { + marginTop: 20, + background: theme.palette.primary.light, + color: theme.palette.secondary.main, + '& div': { + background: colorfull[4], + color: theme.palette.common.white + } + }, + taskStatus: { + display: 'flex', + alignItems: 'center', + '& a': { + textDecoration: 'none', + color: theme.palette.primary.main + } + }, + counterIcon: { + color: theme.palette.common.white, + opacity: 0.7, + fontSize: 84 + }, + progressCircle: { + margin: 20 + }, + itemCarousel: { + textAlign: 'center', + '& img': { + margin: '10px auto' + } + }, + albumRoot: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-around', + overflow: 'hidden', + backgroundColor: theme.palette.background.paper, + }, + gridList: { + height: 'auto', + [theme.breakpoints.up('sm')]: { + width: 500, + }, + }, + icon: { + color: 'rgba(255, 255, 255, 0.54)', + }, + img: { + maxWidth: 'none' + }, + mapWrap: { + position: 'relative' + }, + address: { + [theme.breakpoints.up('md')]: { + top: '50%', + right: 60, + position: 'absolute', + transform: 'translate(0, -50%)' + }, + }, + carouselItem: { + margin: '0 5px', + boxShadow: theme.shadows[3], + borderRadius: 4, + overflow: 'hidden', + height: 250, + padding: '60px 20px', + position: 'relative' + }, + iconBg: { + color: theme.palette.common.white, + opacity: 0.25, + position: 'absolute', + bottom: 10, + right: 10, + fontSize: 96 + }, + carouselTitle: { + color: theme.palette.common.white, + display: 'flex', + flexDirection: 'column', + fontWeight: 500, + fontSize: 20 + }, + carouselDesc: { + color: theme.palette.common.white + }, + chartWrap: { + overflow: 'auto', + }, + chartFluid: { + width: '100%', + minWidth: 400, + height: 300, + } +}); + +export default styles; diff --git a/front/odiparpack/app/components/index.js b/front/odiparpack/app/components/index.js new file mode 100644 index 0000000..9d7bdb3 --- /dev/null +++ b/front/odiparpack/app/components/index.js @@ -0,0 +1,87 @@ +export Header from './Header/Header'; +export Sidebar from './Sidebar/Sidebar'; +export BreadCrumb from './BreadCrumb/BreadCrumb'; +export SourceReader from './SourceReader/SourceReader'; +export PapperBlock from './PapperBlock/PapperBlock'; +// Dashboard and Widget +export CounterWidget from './Counter/CounterWidget'; +export SliderWidget from './Widget/SliderWidget'; +export CounterGroupWidget from './Widget/CounterGroupWidget'; +export CounterIconsWidget from './Widget/CounterIconsWidget'; +export BigChartWidget from './Widget/BigChartWidget'; +export TableWidget from './Widget/TableWidget'; +export TaskWidget from './Widget/TaskWidget'; +export ProfileWidget from './Widget/ProfileWidget'; +export ProgressWidget from './Widget/ProgressWidget'; +export AreaChartWidget from './Widget/AreaChartWidget'; +export CarouselWidget from './Widget/CarouselWidget'; +export AlbumWidget from './Widget/AlbumWidget'; +export MapWidget from './Widget/MapWidget'; +// Table Components +export TreeTable from './Tables/TreeTable'; +export CrudTable from './Tables/CrudTable'; +export CrudTableForm from './Tables/CrudTableForm'; +export AdvTable from './Tables/AdvTable'; +export EmptyData from './Tables/EmptyData'; +// Form +export Notification from './Notification/Notification'; +export MaterialDropZone from './Forms/MaterialDropZone'; +export LoginForm from './Forms/LoginForm'; +export RegisterForm from './Forms/RegisterForm'; +export ResetForm from './Forms/ResetForm'; +export LockForm from './Forms/LockForm'; +// UI +export LimitedBadges from './Badges/LimitedBadges'; +export Quote from './Quote/Quote'; +export Pagination from './Pagination/Pagination'; +export ImageLightbox from './ImageLightbox/ImageLightbox'; +export Rating from './Rating/Rating'; +// Social Media +export Cover from './SocialMedia/Cover'; +export Timeline from './SocialMedia/Timeline'; +export SideSection from './SocialMedia/SideSection'; +export WritePost from './SocialMedia/WritePost'; +// Profile +export About from './Profile/About'; +export Albums from './Profile/Albums'; +export Connection from './Profile/Connection'; +export Favorites from './Profile/Favorites'; +// Card +export ProfileCard from './CardPaper/ProfileCard'; +export GeneralCard from './CardPaper/GeneralCard'; +export NewsCard from './CardPaper/NewsCard'; +export PlayerCard from './CardPaper/PlayerCard'; +export PostCard from './CardPaper/PostCard'; +export ProductCard from './CardPaper/ProductCard'; +export VideoCard from './CardPaper/VideoCard'; +export IdentityCard from './CardPaper/IdentityCard'; +// Search +export SearchProduct from './Search/SearchProduct'; +// Gallery +export ProductGallery from './Gallery/ProductGallery'; +export PhotoGallery from './Gallery/PhotoGallery'; +// Panel +export FloatingPanel from './Panel/FloatingPanel'; +export Cart from './Cart/Cart'; +// Contact +export AddContact from './Contact/AddContact'; +export ContactList from './Contact/ContactList'; +export ContactHeader from './Contact/ContactHeader'; +export ContactDetail from './Contact/ContactDetail'; +// Chat +export ChatHeader from './Chat/ChatHeader'; +export ChatRoom from './Chat/ChatRoom'; +// Email +export EmailHeader from './Email/EmailHeader'; +export EmailSidebar from './Email/EmailSidebar'; +export EmailList from './Email/EmailList'; +export ComposeEmail from './Email/ComposeEmail'; +// Calendar +export EventCalendar from './Calendar/EventCalendar'; +export DetailEvent from './Calendar/DetailEvent'; +export AddEvent from './Calendar/AddEvent'; +export AddEventForm from './Calendar/AddEventForm'; +// Error +export ErrorWrap from './Error/ErrorWrap'; +// Template Settings +export TemplateSettings from './TemplateSettings'; |
