summaryrefslogtreecommitdiffstats
path: root/front/odiparpack/app/components
diff options
context:
space:
mode:
authorDayana31 <[email protected]>2022-04-21 17:27:08 -0500
committerDayana31 <[email protected]>2022-04-21 17:27:08 -0500
commit67c50667678dd0ce4709b29a854f6a47093a1ac5 (patch)
treeb6f9f39092ad54bf6b815984d32b37d7c7ca67ab /front/odiparpack/app/components
parent91140b24f0d49a9f89a080ee063e9eb023a4b73a (diff)
parente13e630cd6e4fc0b1ff92098a28a770794c7bb9a (diff)
downloadDP1_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')
-rw-r--r--front/odiparpack/app/components/.DS_Storebin0 -> 8196 bytes
-rw-r--r--front/odiparpack/app/components/Badges/LimitedBadges.js27
-rw-r--r--front/odiparpack/app/components/BreadCrumb/BreadCrumb.js55
-rw-r--r--front/odiparpack/app/components/BreadCrumb/breadCrumb-jss.js29
-rw-r--r--front/odiparpack/app/components/Calendar/AddEvent.js49
-rw-r--r--front/odiparpack/app/components/Calendar/AddEventForm.js178
-rw-r--r--front/odiparpack/app/components/Calendar/DetailEvent.js167
-rw-r--r--front/odiparpack/app/components/Calendar/EventCalendar.js70
-rw-r--r--front/odiparpack/app/components/Calendar/calendar-jss.js118
-rw-r--r--front/odiparpack/app/components/CardPaper/GeneralCard.js52
-rw-r--r--front/odiparpack/app/components/CardPaper/IdentityCard.js70
-rw-r--r--front/odiparpack/app/components/CardPaper/NewsCard.js46
-rw-r--r--front/odiparpack/app/components/CardPaper/PlayerCard.js66
-rw-r--r--front/odiparpack/app/components/CardPaper/PostCard.js142
-rw-r--r--front/odiparpack/app/components/CardPaper/ProductCard.js134
-rw-r--r--front/odiparpack/app/components/CardPaper/ProfileCard.js99
-rw-r--r--front/odiparpack/app/components/CardPaper/VideoCard.js90
-rw-r--r--front/odiparpack/app/components/CardPaper/cardStyle-jss.js188
-rw-r--r--front/odiparpack/app/components/Cart/Cart.js137
-rw-r--r--front/odiparpack/app/components/Cart/cart-jss.js38
-rw-r--r--front/odiparpack/app/components/Chat/ChatHeader.js122
-rw-r--r--front/odiparpack/app/components/Chat/ChatRoom.js106
-rw-r--r--front/odiparpack/app/components/Chat/MessageField.js69
-rw-r--r--front/odiparpack/app/components/Chat/chatStyle-jss.js148
-rw-r--r--front/odiparpack/app/components/Chat/svg/trigger-opaque.svg66
-rw-r--r--front/odiparpack/app/components/Chat/svg/trigger-transparent.svg68
-rw-r--r--front/odiparpack/app/components/Contact/AddContact.js82
-rw-r--r--front/odiparpack/app/components/Contact/AddContactForm.js273
-rw-r--r--front/odiparpack/app/components/Contact/ContactDetail.js195
-rw-r--r--front/odiparpack/app/components/Contact/ContactHeader.js62
-rw-r--r--front/odiparpack/app/components/Contact/ContactList.js112
-rw-r--r--front/odiparpack/app/components/Contact/contact-jss.js282
-rw-r--r--front/odiparpack/app/components/Counter/CounterWidget.js80
-rw-r--r--front/odiparpack/app/components/Divider/divider-jss.js70
-rw-r--r--front/odiparpack/app/components/Divider/index.js148
-rw-r--r--front/odiparpack/app/components/Email/ComposeEmail.js65
-rw-r--r--front/odiparpack/app/components/Email/ComposeEmailForm.js262
-rw-r--r--front/odiparpack/app/components/Email/EmailHeader.js49
-rw-r--r--front/odiparpack/app/components/Email/EmailList.js314
-rw-r--r--front/odiparpack/app/components/Email/EmailSidebar.js162
-rw-r--r--front/odiparpack/app/components/Email/email-jss.js238
-rw-r--r--front/odiparpack/app/components/Error/ErrorWrap.js75
-rw-r--r--front/odiparpack/app/components/Forms/LockForm.js123
-rw-r--r--front/odiparpack/app/components/Forms/LoginForm.js147
-rw-r--r--front/odiparpack/app/components/Forms/MaterialDropZone.js202
-rw-r--r--front/odiparpack/app/components/Forms/ReduxFormMUI.js69
-rw-r--r--front/odiparpack/app/components/Forms/RegisterForm.js174
-rw-r--r--front/odiparpack/app/components/Forms/ResetForm.js71
-rw-r--r--front/odiparpack/app/components/Forms/helpers/helpers.js8
-rw-r--r--front/odiparpack/app/components/Forms/user-jss.js179
-rw-r--r--front/odiparpack/app/components/Gallery/PhotoGallery.js83
-rw-r--r--front/odiparpack/app/components/Gallery/ProductDetail.js195
-rw-r--r--front/odiparpack/app/components/Gallery/ProductGallery.js152
-rw-r--r--front/odiparpack/app/components/Gallery/photo-jss.js79
-rw-r--r--front/odiparpack/app/components/Gallery/product-jss.js87
-rw-r--r--front/odiparpack/app/components/Header/Header.js63
-rw-r--r--front/odiparpack/app/components/Header/UserMenu.js177
-rw-r--r--front/odiparpack/app/components/Header/header-jss.js166
-rw-r--r--front/odiparpack/app/components/ImageLightbox/ImageLightbox.js1794
-rw-r--r--front/odiparpack/app/components/ImageLightbox/constant.js39
-rw-r--r--front/odiparpack/app/components/ImageLightbox/util.js46
-rw-r--r--front/odiparpack/app/components/Loading/index.js20
-rw-r--r--front/odiparpack/app/components/Notification/Notification.js59
-rw-r--r--front/odiparpack/app/components/Pagination/Pagination.js189
-rw-r--r--front/odiparpack/app/components/Panel/FloatingPanel.js90
-rw-r--r--front/odiparpack/app/components/Panel/panel-jss.js95
-rw-r--r--front/odiparpack/app/components/PapperBlock/PapperBlock.js55
-rw-r--r--front/odiparpack/app/components/PapperBlock/papperStyle-jss.js58
-rw-r--r--front/odiparpack/app/components/Profile/About.js243
-rw-r--r--front/odiparpack/app/components/Profile/Albums.js152
-rw-r--r--front/odiparpack/app/components/Profile/Connection.js45
-rw-r--r--front/odiparpack/app/components/Profile/Favorites.js130
-rw-r--r--front/odiparpack/app/components/Profile/profile-jss.js155
-rw-r--r--front/odiparpack/app/components/Quote/Quote.js81
-rw-r--r--front/odiparpack/app/components/Rating/Rating.js144
-rw-r--r--front/odiparpack/app/components/Search/SearchProduct.js84
-rw-r--r--front/odiparpack/app/components/Search/search-jss.js48
-rw-r--r--front/odiparpack/app/components/Sidebar/MainMenu.js130
-rw-r--r--front/odiparpack/app/components/Sidebar/OtherMenu.js44
-rw-r--r--front/odiparpack/app/components/Sidebar/Sidebar.js122
-rw-r--r--front/odiparpack/app/components/Sidebar/sidebar-jss.js205
-rw-r--r--front/odiparpack/app/components/SocialMedia/Comment.js135
-rw-r--r--front/odiparpack/app/components/SocialMedia/Cover.js103
-rw-r--r--front/odiparpack/app/components/SocialMedia/SideSection.js209
-rw-r--r--front/odiparpack/app/components/SocialMedia/Timeline.js177
-rw-r--r--front/odiparpack/app/components/SocialMedia/WritePost.js169
-rw-r--r--front/odiparpack/app/components/SocialMedia/jss/cover-jss.js55
-rw-r--r--front/odiparpack/app/components/SocialMedia/jss/socialMedia-jss.js89
-rw-r--r--front/odiparpack/app/components/SocialMedia/jss/timeline-jss.js95
-rw-r--r--front/odiparpack/app/components/SocialMedia/jss/writePost-jss.js73
-rw-r--r--front/odiparpack/app/components/SourceReader/SourceReader.js114
-rw-r--r--front/odiparpack/app/components/Tables/AdvTable.js206
-rw-r--r--front/odiparpack/app/components/Tables/CrudTable.js52
-rw-r--r--front/odiparpack/app/components/Tables/CrudTableForm.js70
-rw-r--r--front/odiparpack/app/components/Tables/EmptyData.js14
-rw-r--r--front/odiparpack/app/components/Tables/TreeTable.js190
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/DatePickerCell.js59
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/EditableCell.js86
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/Form.js71
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/MainTable.js104
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/MainTableForm.js97
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/Row.js167
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/RowReadOnly.js76
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/SelectableCell.js46
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/TableHeader.js74
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/TableToolbar.js123
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/TimePickerCell.js66
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/ToggleCell.js50
-rw-r--r--front/odiparpack/app/components/Tables/tableParts/tableStyle-jss.js63
-rw-r--r--front/odiparpack/app/components/TemplateSettings/ThemeThumb.js74
-rw-r--r--front/odiparpack/app/components/TemplateSettings/index.js93
-rw-r--r--front/odiparpack/app/components/TemplateSettings/themeStyles-jss.js106
-rw-r--r--front/odiparpack/app/components/Widget/AlbumWidget.js55
-rw-r--r--front/odiparpack/app/components/Widget/AreaChartWidget.js147
-rw-r--r--front/odiparpack/app/components/Widget/BigChartWidget.js142
-rw-r--r--front/odiparpack/app/components/Widget/CarouselWidget.js117
-rw-r--r--front/odiparpack/app/components/Widget/CounterGroupWidget.js101
-rw-r--r--front/odiparpack/app/components/Widget/CounterIconsWidget.js74
-rw-r--r--front/odiparpack/app/components/Widget/MapWidget.js60
-rw-r--r--front/odiparpack/app/components/Widget/ProfileWidget.js54
-rw-r--r--front/odiparpack/app/components/Widget/ProgressWidget.js39
-rw-r--r--front/odiparpack/app/components/Widget/SliderWidget.js69
-rw-r--r--front/odiparpack/app/components/Widget/TableWidget.js124
-rw-r--r--front/odiparpack/app/components/Widget/TaskWidget.js83
-rw-r--r--front/odiparpack/app/components/Widget/widget-jss.js294
-rw-r--r--front/odiparpack/app/components/index.js87
126 files changed, 15358 insertions, 0 deletions
diff --git a/front/odiparpack/app/components/.DS_Store b/front/odiparpack/app/components/.DS_Store
new file mode 100644
index 0000000..a82440f
--- /dev/null
+++ b/front/odiparpack/app/components/.DS_Store
Binary files differ
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>
+ );
+ })
+ }
+ &nbsp;{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>
+ &nbsp;
+ <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 &amp; 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}
+ &nbsp;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';