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 {
|
.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;
|
display: flex;
|
||||||
|
|
||||||
> aside.aside {
|
> aside.aside {
|
||||||
@ -253,8 +283,9 @@ button {
|
|||||||
textarea {
|
textarea {
|
||||||
border: 1px solid $color-secondary;
|
border: 1px solid $color-secondary;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 .5rem;
|
||||||
padding: .4rem;
|
padding: .4rem;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
@ -264,6 +295,16 @@ button {
|
|||||||
padding: .7rem;
|
padding: .7rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 3px;
|
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 data = window.shop_data;
|
||||||
|
|
||||||
|
const axios = window.axios;
|
||||||
|
|
||||||
const productStyle = (style, snapshot, removeAnim, hovered, selected) => {
|
const productStyle = (style, snapshot, removeAnim, hovered, selected) => {
|
||||||
const custom = {
|
const custom = {
|
||||||
@ -140,6 +141,7 @@ class Layout extends React.PureComponent {
|
|||||||
isMobile: PropTypes.bool,
|
isMobile: PropTypes.bool,
|
||||||
newCardJustAdded: PropTypes.bool,
|
newCardJustAdded: PropTypes.bool,
|
||||||
onClickToggleMobileSideMenu: PropTypes.func,
|
onClickToggleMobileSideMenu: PropTypes.func,
|
||||||
|
onClickCloseRFQFeedback: PropTypes.func,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +158,9 @@ class Layout extends React.PureComponent {
|
|||||||
mobileSideMenuShouldOpen,
|
mobileSideMenuShouldOpen,
|
||||||
isMobile,
|
isMobile,
|
||||||
newCardJustAdded,
|
newCardJustAdded,
|
||||||
onClickToggleMobileSideMenu
|
onClickToggleMobileSideMenu,
|
||||||
|
onClickCloseRFQFeedback,
|
||||||
|
showRFQFeedback,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -176,6 +180,23 @@ class Layout extends React.PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -812,43 +833,197 @@ class OrderForm extends React.PureComponent {
|
|||||||
|
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
|
isProcessing: PropTypes.bool,
|
||||||
|
isProcessingComplete: PropTypes.bool,
|
||||||
onClickSubmit: PropTypes.func,
|
onClickSubmit: PropTypes.func,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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.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({
|
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) {
|
handleSubmit(event) {
|
||||||
if (this.props.onClickSubmit) {
|
|
||||||
this.props.onClickSubmit(this.state.note);
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
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() {
|
render() {
|
||||||
|
const {
|
||||||
|
handleEmail,
|
||||||
|
handleNote,
|
||||||
|
resetEmptyError,
|
||||||
|
handleSubmit,
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
note,
|
||||||
|
error,
|
||||||
|
empty
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const { isProcessing, isProcessingComplete } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="summary-form">
|
<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
|
<textarea
|
||||||
value={this.state.note}
|
onChange={handleNote}
|
||||||
onChange={this.handleNoteChange}
|
value={note}
|
||||||
rows="5"
|
rows="5"
|
||||||
placeholder="Additional notes" />
|
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>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -1174,6 +1349,7 @@ class Shop extends React.PureComponent {
|
|||||||
this.handleClickSubmit = this.handleClickSubmit.bind(this);
|
this.handleClickSubmit = this.handleClickSubmit.bind(this);
|
||||||
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
|
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
|
||||||
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
|
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
|
||||||
|
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
|
||||||
|
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
}
|
}
|
||||||
@ -1375,7 +1551,7 @@ class Shop extends React.PureComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickSubmit(note) {
|
handleClickSubmit(note, email) {
|
||||||
const crate = {
|
const crate = {
|
||||||
items: [],
|
items: [],
|
||||||
type: this.state.currentMode,
|
type: this.state.currentMode,
|
||||||
@ -1388,15 +1564,33 @@ class Shop extends React.PureComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {data} = this.props;
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
const num = (new Date()).getTime();
|
const num = (new Date()).getTime();
|
||||||
const subject = `[Order hardware] - Request Quote`;
|
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) {
|
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);
|
document.body.appendChild(a);
|
||||||
|
|
||||||
a.style = 'display: none';
|
a.style = 'display: none';
|
||||||
@ -1484,6 +1678,12 @@ class Shop extends React.PureComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleClickCloseRFQFeedback() {
|
||||||
|
this.setState({
|
||||||
|
shouldShowRFQFeedback: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
checkAlerts(prevItems, newItems) {
|
checkAlerts(prevItems, newItems) {
|
||||||
console.log('--- START CHECKING CRATE WARNING ---');
|
console.log('--- START CHECKING CRATE WARNING ---');
|
||||||
|
|
||||||
@ -1802,6 +2002,9 @@ class Shop extends React.PureComponent {
|
|||||||
rules,
|
rules,
|
||||||
mobileSideMenuShouldOpen,
|
mobileSideMenuShouldOpen,
|
||||||
newCardJustAdded,
|
newCardJustAdded,
|
||||||
|
isProcessing,
|
||||||
|
shouldShowRFQFeedback,
|
||||||
|
isProcessingComplete,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const isMobile = window.deviceIsMobile();
|
const isMobile = window.deviceIsMobile();
|
||||||
@ -1810,11 +2013,13 @@ class Shop extends React.PureComponent {
|
|||||||
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
|
showRFQFeedback={shouldShowRFQFeedback}
|
||||||
className="shop"
|
className="shop"
|
||||||
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
|
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
newCardJustAdded={newCardJustAdded}
|
newCardJustAdded={newCardJustAdded}
|
||||||
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
||||||
|
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
|
||||||
aside={
|
aside={
|
||||||
<Backlog
|
<Backlog
|
||||||
currency={currency}
|
currency={currency}
|
||||||
@ -1871,6 +2076,9 @@ class Shop extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
form={
|
form={
|
||||||
<OrderForm
|
<OrderForm
|
||||||
|
isProcessingComplete={isProcessingComplete}
|
||||||
|
processingComplete={this.handleProcessingComplete}
|
||||||
|
isProcessing={isProcessing}
|
||||||
onClickSubmit={this.handleClickSubmit}>
|
onClickSubmit={this.handleClickSubmit}>
|
||||||
</OrderForm>
|
</OrderForm>
|
||||||
}>
|
}>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const shop_data = {
|
const shop_data = {
|
||||||
|
|
||||||
|
API_RFQ: 'http://127.0.0.1:5000/api/rfq',
|
||||||
|
|
||||||
mobileSideMenuShouldOpen: false,
|
mobileSideMenuShouldOpen: false,
|
||||||
currentItemHovered: null,
|
currentItemHovered: null,
|
||||||
currentMode: 'rack',
|
currentMode: 'rack',
|
||||||
|
@ -67,6 +67,7 @@
|
|||||||
<!-- v11.0.5 -->
|
<!-- v11.0.5 -->
|
||||||
<script src="{{ get_url(path='js/react-beautiful-dnd.min.js', cachebust=true) }}"></script>
|
<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/uuid_v4@latest.js', cachebust=true) }}"></script>
|
||||||
|
<script src="{{ get_url(path='js/axios.min.js', cachebust=true) }}"></script>
|
||||||
|
|
||||||
<!-- Load Data -->
|
<!-- Load Data -->
|
||||||
<script src="{{ get_url(path='js/shop_data.js', cachebust=true) }}"></script>
|
<script src="{{ get_url(path='js/shop_data.js', cachebust=true) }}"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user