forked from M-Labs/web2019
feat(issue22/UI): Updates shop (email + req to API for RFQ + feeback)
This commit is contained in:
parent
64730e8557
commit
0c4d2cfdac
@ -12,6 +12,36 @@ button {
|
||||
|
||||
.layout {
|
||||
|
||||
.rfqFeedback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 3rem;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 350px;
|
||||
background: white;
|
||||
left: calc(100%/2 - 350px/2);
|
||||
-webkit-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
-moz-box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
box-shadow: 0px 0px 33px -7px rgba(0,0,0,0.75);
|
||||
top: calc(50% - 50px);
|
||||
border: 1px solid $brand-color;
|
||||
font-size: .9rem;
|
||||
|
||||
button {
|
||||
background-color: inherit;
|
||||
align-self: center;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
img {
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
|
||||
> aside.aside {
|
||||
@ -253,8 +283,9 @@ button {
|
||||
textarea {
|
||||
border: 1px solid $color-secondary;
|
||||
border-radius: 3px;
|
||||
margin: 0 0 1rem;
|
||||
margin: 0 0 .5rem;
|
||||
padding: .4rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
@ -264,6 +295,16 @@ button {
|
||||
padding: .7rem;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e53e3e;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.errorField {
|
||||
border: 1px solid #e53e3e !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
static/images/shop/icon-close.svg
Normal file
7
static/images/shop/icon-close.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
|
||||
<g id="Group_441" data-name="Group 441" transform="translate(-1251 -346)">
|
||||
<rect id="Rectangle_1020" data-name="Rectangle 1020" width="22" height="22" transform="translate(1251 346)" fill="none"/>
|
||||
<rect id="Rectangle_1021" data-name="Rectangle 1021" width="2.4" height="24" transform="translate(1269.778 347.808) rotate(45)" fill="#715ec7"/>
|
||||
<rect id="Rectangle_1022" data-name="Rectangle 1022" width="2.4" height="24" transform="translate(1271.192 364.778) rotate(135)" fill="#715ec7"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 600 B |
4
static/images/shop/icon-done.svg
Normal file
4
static/images/shop/icon-done.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg id="done" xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50">
|
||||
<path id="Path_795" data-name="Path 795" d="M0,0H50V50H0Z" fill="none"/>
|
||||
<path id="Path_796" data-name="Path 796" d="M22.833,2A20.833,20.833,0,1,0,43.667,22.833,20.841,20.841,0,0,0,22.833,2ZM18.667,33.25,8.25,22.833,11.187,19.9l7.479,7.458L34.479,11.542,37.417,14.5Z" transform="translate(2.167 2.167)" fill="#715ec7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 425 B |
3
static/js/axios.min.js
vendored
Normal file
3
static/js/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -8,6 +8,7 @@ const {
|
||||
|
||||
const data = window.shop_data;
|
||||
|
||||
const axios = window.axios;
|
||||
|
||||
const productStyle = (style, snapshot, removeAnim, hovered, selected) => {
|
||||
const custom = {
|
||||
@ -140,6 +141,7 @@ class Layout extends React.PureComponent {
|
||||
isMobile: PropTypes.bool,
|
||||
newCardJustAdded: PropTypes.bool,
|
||||
onClickToggleMobileSideMenu: PropTypes.func,
|
||||
onClickCloseRFQFeedback: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
@ -156,7 +158,9 @@ class Layout extends React.PureComponent {
|
||||
mobileSideMenuShouldOpen,
|
||||
isMobile,
|
||||
newCardJustAdded,
|
||||
onClickToggleMobileSideMenu
|
||||
onClickToggleMobileSideMenu,
|
||||
onClickCloseRFQFeedback,
|
||||
showRFQFeedback,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -176,6 +180,23 @@ class Layout extends React.PureComponent {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
<div className="rfqFeedback" style={{'display': `${ showRFQFeedback ? 'flex' : 'none'}`}}>
|
||||
<div>
|
||||
<img width="30px" src="/images/shop/icon-done.svg" alt="close" />
|
||||
</div>
|
||||
|
||||
<div style={{'padding': '0 .5em'}}>
|
||||
We've received your request and will be in contact soon.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button onClick={onClickCloseRFQFeedback}>
|
||||
<img src="/images/shop/icon-close.svg" alt="close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -812,43 +833,197 @@ class OrderForm extends React.PureComponent {
|
||||
|
||||
static get propTypes() {
|
||||
return {
|
||||
isProcessing: PropTypes.bool,
|
||||
isProcessingComplete: PropTypes.bool,
|
||||
onClickSubmit: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {note: ''};
|
||||
this.state = {
|
||||
note: '',
|
||||
email: '',
|
||||
error: {
|
||||
note: null,
|
||||
email: null,
|
||||
},
|
||||
empty: {
|
||||
note: null,
|
||||
email: null,
|
||||
},
|
||||
};
|
||||
|
||||
this.handleNoteChange = this.handleNoteChange.bind(this);
|
||||
this.handleEmail = this.handleEmail.bind(this);
|
||||
this.handleNote = this.handleNote.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.resetEmptyError = this.resetEmptyError.bind(this);
|
||||
this.checkValidation = this.checkValidation.bind(this);
|
||||
}
|
||||
|
||||
handleNoteChange(event) {
|
||||
checkValidation() {
|
||||
let isValid = true;
|
||||
let validationFields = {...this.state};
|
||||
|
||||
const {
|
||||
isEmpty: isEmailEmpty,
|
||||
isError: isEmailError
|
||||
} = this.validateEmail(this.state.email);
|
||||
|
||||
validationFields = {
|
||||
...validationFields,
|
||||
error: {
|
||||
...this.state.error,
|
||||
email: isEmailError,
|
||||
},
|
||||
empty: {
|
||||
...this.state.empty,
|
||||
email: isEmailEmpty,
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(validationFields);
|
||||
|
||||
isValid =
|
||||
!isEmailEmpty &&
|
||||
!isEmailError
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
validateEmail(value) {
|
||||
let isEmpty = null;
|
||||
let isError = null;
|
||||
|
||||
const { t } = this.props;
|
||||
|
||||
if (!value || value.trim() === '') {
|
||||
isEmpty = true;
|
||||
} else if (value && !value.match(/^\w+([\+\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)) {
|
||||
isError = {
|
||||
message: 'Your email is incomplete',
|
||||
};
|
||||
}
|
||||
|
||||
return { isEmpty, isError };
|
||||
}
|
||||
|
||||
validateNote(value) {
|
||||
let isEmpty = null;
|
||||
|
||||
if (!value || value.trim() === '') {
|
||||
isEmpty = true;
|
||||
}
|
||||
|
||||
return { isEmpty };
|
||||
}
|
||||
|
||||
resetEmptyError(key) {
|
||||
this.setState({
|
||||
note: event.target.value,
|
||||
...this.state,
|
||||
error: {
|
||||
...this.state.error,
|
||||
[key]: null,
|
||||
},
|
||||
empty: {
|
||||
...this.state.empty,
|
||||
[key]: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleEmail(e) {
|
||||
const value = e.target.value;
|
||||
const { isEmpty, isError } = this.validateEmail(value);
|
||||
|
||||
this.setState({
|
||||
...this.state,
|
||||
email: value,
|
||||
error: {
|
||||
...this.state.error,
|
||||
email: isError,
|
||||
},
|
||||
empty: {
|
||||
...this.state.empty,
|
||||
email: isEmpty,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleNote(e) {
|
||||
const value = e.target.value;
|
||||
|
||||
this.setState({
|
||||
...this.state,
|
||||
note: value,
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
if (this.props.onClickSubmit) {
|
||||
this.props.onClickSubmit(this.state.note);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
if (this.props.onClickSubmit) {
|
||||
// check validation input fields
|
||||
const isValidated = this.checkValidation();
|
||||
if (!isValidated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.props.onClickSubmit(this.state.note, this.state.email);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
handleEmail,
|
||||
handleNote,
|
||||
resetEmptyError,
|
||||
handleSubmit,
|
||||
} = this;
|
||||
|
||||
const {
|
||||
email,
|
||||
note,
|
||||
error,
|
||||
empty
|
||||
} = this.state;
|
||||
|
||||
const { isProcessing, isProcessingComplete } = this.props;
|
||||
|
||||
return (
|
||||
<div className="summary-form">
|
||||
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
|
||||
<input
|
||||
className={`${error && error.email ? 'errorField':''}`}
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
onFocus={() => resetEmptyError('email')}
|
||||
onChange={handleEmail}
|
||||
onBlur={handleEmail}
|
||||
value={email} />
|
||||
|
||||
{ empty && empty.email ? (
|
||||
<div className="error">
|
||||
<small>Required</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{ error && error.email ? (
|
||||
<div className="error">
|
||||
<small>Your email is incomplete</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<textarea
|
||||
value={this.state.note}
|
||||
onChange={this.handleNoteChange}
|
||||
onChange={handleNote}
|
||||
value={note}
|
||||
rows="5"
|
||||
placeholder="Additional notes" />
|
||||
<input type="submit" value="Request quote" />
|
||||
This will open an email window. Send the email to make your request.
|
||||
|
||||
<input style={{'backgroundColor': `${isProcessingComplete ? 'gray':'#715ec7'}`}} disabled={isProcessingComplete} type="submit" value={`${isProcessing ? 'Processing ...' : 'Request quote'}`} />
|
||||
{/*This will open an email window. Send the email to make your request.*/}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@ -1174,6 +1349,7 @@ class Shop extends React.PureComponent {
|
||||
this.handleClickSubmit = this.handleClickSubmit.bind(this);
|
||||
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
|
||||
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
|
||||
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
|
||||
|
||||
this.timer = null;
|
||||
}
|
||||
@ -1375,7 +1551,7 @@ class Shop extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
handleClickSubmit(note) {
|
||||
handleClickSubmit(note, email) {
|
||||
const crate = {
|
||||
items: [],
|
||||
type: this.state.currentMode,
|
||||
@ -1388,15 +1564,33 @@ class Shop extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
const {data} = this.props;
|
||||
|
||||
const a = document.createElement('a');
|
||||
const num = (new Date()).getTime();
|
||||
const subject = `[Order hardware] - Request Quote`;
|
||||
let body = `Hello!\n\nI would like to request a quotation for my below configuration:\n\n${JSON.stringify(crate)}\n\n(Please do not edit the machine-readable representation above)\n\n`;
|
||||
let body = `Hello!<br><br>I would like to request a quotation for my below configuration:<br><br>${JSON.stringify(crate)}<br><br>(Please do not edit the machine-readable representation above)<br><br>`;
|
||||
|
||||
if (note) {
|
||||
body = `${body}\n\nAdditional note:\n\n${note ? note.trim() : ''}`;
|
||||
body = `${body}<br><br>Additional note:<br><br>${note ? note.trim() : ''}`;
|
||||
}
|
||||
|
||||
this.setState({isProcessing: true});
|
||||
|
||||
axios.post(data.API_RFQ, {
|
||||
email,
|
||||
body,
|
||||
headers: {'X-MLABS-OH': 'rlebcleu'}
|
||||
}).then(response => {
|
||||
this.setState({isProcessing: false, shouldShowRFQFeedback: true, isProcessingComplete: true});
|
||||
}).catch(err => {
|
||||
this.setState({isProcessing: false}, () => {
|
||||
alert("We cannot receive your request. Try using the export by coping the configuration and send it to us at sales[at]m-labs.hk");
|
||||
});
|
||||
})
|
||||
|
||||
return;
|
||||
|
||||
document.body.appendChild(a);
|
||||
|
||||
a.style = 'display: none';
|
||||
@ -1484,6 +1678,12 @@ class Shop extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
handleClickCloseRFQFeedback() {
|
||||
this.setState({
|
||||
shouldShowRFQFeedback: false,
|
||||
});
|
||||
}
|
||||
|
||||
checkAlerts(prevItems, newItems) {
|
||||
console.log('--- START CHECKING CRATE WARNING ---');
|
||||
|
||||
@ -1802,6 +2002,9 @@ class Shop extends React.PureComponent {
|
||||
rules,
|
||||
mobileSideMenuShouldOpen,
|
||||
newCardJustAdded,
|
||||
isProcessing,
|
||||
shouldShowRFQFeedback,
|
||||
isProcessingComplete,
|
||||
} = this.state;
|
||||
|
||||
const isMobile = window.deviceIsMobile();
|
||||
@ -1810,11 +2013,13 @@ class Shop extends React.PureComponent {
|
||||
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
||||
|
||||
<Layout
|
||||
showRFQFeedback={shouldShowRFQFeedback}
|
||||
className="shop"
|
||||
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
|
||||
isMobile={isMobile}
|
||||
newCardJustAdded={newCardJustAdded}
|
||||
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
||||
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
|
||||
aside={
|
||||
<Backlog
|
||||
currency={currency}
|
||||
@ -1871,6 +2076,9 @@ class Shop extends React.PureComponent {
|
||||
}
|
||||
form={
|
||||
<OrderForm
|
||||
isProcessingComplete={isProcessingComplete}
|
||||
processingComplete={this.handleProcessingComplete}
|
||||
isProcessing={isProcessing}
|
||||
onClickSubmit={this.handleClickSubmit}>
|
||||
</OrderForm>
|
||||
}>
|
||||
|
@ -1,5 +1,7 @@
|
||||
const shop_data = {
|
||||
|
||||
API_RFQ: 'http://127.0.0.1:5000/api/rfq',
|
||||
|
||||
mobileSideMenuShouldOpen: false,
|
||||
currentItemHovered: null,
|
||||
currentMode: 'rack',
|
||||
|
@ -67,6 +67,7 @@
|
||||
<!-- v11.0.5 -->
|
||||
<script src="{{ get_url(path='js/react-beautiful-dnd.min.js', cachebust=true) }}"></script>
|
||||
<script src="{{ get_url(path='js/uuid_v4@latest.js', cachebust=true) }}"></script>
|
||||
<script src="{{ get_url(path='js/axios.min.js', cachebust=true) }}"></script>
|
||||
|
||||
<!-- Load Data -->
|
||||
<script src="{{ get_url(path='js/shop_data.js', cachebust=true) }}"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user