Restored all the functionality, but needs UI fixes

Signed-off-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
Egor Savkin 2023-12-13 12:39:15 +08:00
parent 59f726e805
commit ff7eac97dc
10 changed files with 251 additions and 46 deletions

View File

@ -1,5 +1,71 @@
import {useShopStore} from "./shop_store";
import {useClickAway} from "./options/useClickAway";
import {Modal} from "react-bootstrap";
import React from "react";
import {Validation} from "./validate";
const JSONExample = JSON.stringify({
"items": [{"pn": "1124"}, {"pn": "2118"}, {"pn": "2118"}, {"pn": "2128"}],
"type": "desktop"
});
export function ImportJSON() { export function ImportJSON() {
return ( const {shouldShow, data, loadDescription, updateImportDescription, closeImport, showImport} = useShopStore(state => ({
<div> Import JSON PLACEHOLDER</div> shouldShow: state.importShouldOpen,
) data: state.importValue,
loadDescription: state.loadDescription,
updateImportDescription: state.updateImportDescription,
closeImport: state.closeImport,
showImport: state.openImport,
}));
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
closeImport()
}
);
return (<>
<button
className="btn btn-sm btn-outline-primary m-0 mb-2"
style={{'cursor': 'pointer'}}
onClick={showImport}>Import JSON
</button>
<Modal show={shouldShow} animation={true} centered>
<Modal.Body ref={ref}>
<div className="form-group">
<p className="small">
Input the JSON description below. Should be something like:
<br/>
{JSONExample}
</p>
</div>
<div className="form-group w-100">
<textarea
onChange={(event) => {
console.log(event)
updateImportDescription(event.target.value)
}}
value={data.value}
className="form-control w-100"
rows="5"
placeholder="Input JSON description here."/>
</div>
{data.error !== Validation.OK ? (
<div className="form-group">
<p className="text-danger">{data.error === Validation.Empty ? "Empty input" : "Invalid JSON"}</p>
</div>
) : null}
<div className="d-flex flex-column flex-sm-row justify-content-end">
<a type="button" onClick={closeImport}
className="btn btn-sm btn-outline-primary m-0 mb-2 mb-sm-0 mr-sm-2">Close</a>
<a type="button" onClick={loadDescription}
className={`btn btn-sm btn-primary m-0 ml-sm-2 ${data.error ? 'disabled' : ''}`}>Load
configuration</a>
</div>
</Modal.Body>
</Modal>
</>)
} }

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {validateEmail, Validation} from "./validate.js"; import {validateEmail, Validation} from "./validate.js";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {ShowJSON} from "./ShowJSON";
/** /**
* Components that renders the form to request quote. * Components that renders the form to request quote.
@ -10,20 +11,18 @@ export function OrderForm() {
email, email,
note, note,
isProcessing, isProcessing,
isProcessingComplete,
showDescription,
updateEmail, updateEmail,
updateNote, updateNote,
submitForm, submitForm,
submitDisabled,
} = useShopStore(state => ({ } = useShopStore(state => ({
email: state.email, email: state.email,
note: state.note, note: state.note,
isProcessing: state.isProcessing, isProcessing: state.isProcessing,
isProcessingComplete: state.isProcessingComplete,
showDescription: state.showDescription,
updateEmail: state.updateEmail, updateEmail: state.updateEmail,
updateNote: state.updateNote, updateNote: state.updateNote,
submitForm: state.submitForm, submitForm: state.submitForm,
submitDisabled: state.submitDisabled
})); }));
@ -59,14 +58,10 @@ export function OrderForm() {
placeholder="Additional notes"/> placeholder="Additional notes"/>
<div className="d-flex flex-column flex-sm-row justify-content-between"> <div className="d-flex flex-column flex-sm-row justify-content-between">
<input <ShowJSON/>
className="btn btn-outline-primary w-100 m-0 mb-2 mb-sm-0 me-sm-2"
style={{'cursor': 'pointer', 'fontWeight': '700'}}
defaultValue="Show JSON"
onClick={showDescription}
readOnly={true}/>
<input className="btn btn-primary w-100 m-0 ms-sm-2" type="submit" <input className="btn btn-primary w-100 m-0 ms-sm-2" type="submit"
disabled={submitDisabled()}
value={`${isProcessing ? 'Processing ...' : 'Request quote'}`}/> value={`${isProcessing ? 'Processing ...' : 'Request quote'}`}/>
</div> </div>
{/*This will open an email window. Send the email to make your request.*/} {/*This will open an email window. Send the email to make your request.*/}

View File

@ -3,6 +3,8 @@ import {OrderSummary} from "./OrderSummary";
import {OrderForm} from "./OrderForm"; import {OrderForm} from "./OrderForm";
import {CrateList} from "./CrateList"; import {CrateList} from "./CrateList";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {ImportJSON} from "./ImportJSON";
import {RFQFeedback} from "./RFQFeedback";
/** /**
* Component that renders all things for order. * Component that renders all things for order.
@ -22,13 +24,11 @@ export function OrderPanel({title, description}) {
</div> </div>
<div> <div>
<button <ImportJSON/>
className="btn btn-sm btn-outline-primary m-0 mb-2"
style={{'cursor': 'pointer'}}
onClick={onClickOpenImport}>Import JSON
</button>
</div> </div>
<RFQFeedback/>
{isMobile ? ( {isMobile ? (
<div className="mobileBtnDisplaySideMenu"> <div className="mobileBtnDisplaySideMenu">
<button onClick={onClickToggleMobileSideMenu}> <button onClick={onClickToggleMobileSideMenu}>

View File

@ -0,0 +1,36 @@
import {useShopStore} from "./shop_store";
import {useClickAway} from "./options/useClickAway";
import {Modal} from "react-bootstrap";
import {Validation} from "./validate";
import React from "react";
export function RFQFeedback() {
const {closeRFQ, shouldShow, status} = useShopStore(state => ({
closeRFQ: state.closeRFQFeedback,
shouldShow: state.shouldShowRFQFeedback,
status: state.processingResult
}))
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
closeRFQ()
});
return (
<Modal show={shouldShow} animation={true} centered>
<Modal.Body ref={ref}>
<div className="d-flex">
<div>
{status.status === Validation.OK ?
<img width="30px" src="/images/shop/icon-done.svg" alt="close"/>
: <img width="30px" src="/images/shop/icon-warning.svg" alt="close"/>
}
</div>
<div style={{'padding': '0 .5em'}}>
{status.message}
</div>
</div>
</Modal.Body>
</Modal>
)
}

View File

@ -8,7 +8,7 @@ import {OrderPanel} from "./OrderPanel";
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
/** /**
* Component that render the entire shop * Component that renders the entire shop
*/ */
export function Shop() { export function Shop() {
@ -18,7 +18,7 @@ export function Shop() {
deleteCard: state.deleteCard, deleteCard: state.deleteCard,
cardIndexById: state.cardIndexById cardIndexById: state.cardIndexById
})); }));
const handleOnDragEnd = (drop_result, provided) => { const handleOnDragEnd = (drop_result, _provided) => {
if (drop_result.source.droppableId === "backlog") if (drop_result.source.droppableId === "backlog")
addCardFromBacklog(drop_result.destination.droppableId, drop_result.source.index, drop_result.destination.index); addCardFromBacklog(drop_result.destination.droppableId, drop_result.source.index, drop_result.destination.index);
else if(drop_result.destination.droppableId === "backlog") else if(drop_result.destination.droppableId === "backlog")
@ -28,7 +28,7 @@ export function Shop() {
} }
useEffect(() => { useEffect(() => {
addCardFromBacklog(null, [cardIndexById("kasli"), cardIndexById("eem_pwr_mod")], -1); addCardFromBacklog(null, [cardIndexById("eem_pwr_mod"), cardIndexById("kasli")], -1);
}, []); }, []);
return ( return (

View File

@ -1,5 +1,43 @@
import React from "react";
import {Modal} from "react-bootstrap";
import {useShopStore} from "./shop_store";
import {useClickAway} from "./options/useClickAway";
export function ShowJSON() { export function ShowJSON() {
return ( const {shouldShow, description, showDescription, closeDescription} = useShopStore(state => ({
<div> SHOW JSON PLACEHOLDER</div> shouldShow: state.shouldShowDescription,
) description: state.description,
closeDescription: state.closeDescription,
showDescription: state.showDescription,
}));
const ref = useClickAway((e) => {
if (e.type === "mousedown") // ignore touchstart
closeDescription()
}
);
return (<>
<input
className="btn btn-outline-primary w-100 m-0 mb-2 mb-sm-0 me-sm-2"
style={{'cursor': 'pointer', 'fontWeight': '700'}}
defaultValue="Show JSON"
onClick={showDescription}
readOnly={true}/>
<Modal show={shouldShow} animation={true} centered>
<Modal.Body ref={ref}>
<textarea
value={description}
className="form-control w-100"
rows={10}
readOnly={true}
placeholder="There should be description of the crate"/>
<div className="d-flex flex-column flex-sm-row justify-content-end m-1">
<a type="button" onClick={closeDescription}
className="btn btn-sm btn-outline-primary m-0 mb-2 mb-sm-0 mr-sm-2">Close</a>
</div>
</Modal.Body>
</Modal>
</>)
} }

View File

@ -1,4 +1,6 @@
import {useShopStore} from "./shop_store"; import {useShopStore} from "./shop_store";
import {FilterOptions} from "./options/utils";
import {v4 as uuidv4} from "uuid";
export function validateJSON(description) { export function validateJSON(description) {
@ -11,12 +13,16 @@ export function validateJSON(description) {
const crate_modes = useShopStore.getState().crate_modes; const crate_modes = useShopStore.getState().crate_modes;
const pn_to_card = useShopStore.getState().pn_to_cards; const pn_to_card = useShopStore.getState().pn_to_cards;
try {
for (const crate of crates_raw) { for (const crate of crates_raw) {
if (!crate.type || !crate.items || !(crate.type in crate_modes)) return false; if (!crate.type || !crate.items || !(crate.type in crate_modes)) return false;
for (const card of crate.items) { for (const card of crate.items) {
if (!(card.pn in pn_to_card)) return false; if (!(card.pn in pn_to_card)) return false;
} }
} }
} catch (e) {
return false;
}
return true; return true;
} }
@ -29,6 +35,7 @@ export function JSONToCrates(description) {
crate_mode: crate.type, crate_mode: crate.type,
items: Array.from(crate.items.map((card, i) => ({ items: Array.from(crate.items.map((card, i) => ({
...pn_to_card(card.pn), ...pn_to_card(card.pn),
id: uuidv4(),
options_data: card.options options_data: card.options
}))), }))),
warnings: [], warnings: [],

View File

@ -6,8 +6,8 @@ import {true_type_of} from "./options/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {FillResources} from "./count_resources"; import {FillResources} from "./count_resources";
import {TriggerCrateWarnings, TriggerWarnings} from "./warnings"; import {TriggerCrateWarnings, TriggerWarnings} from "./warnings";
import {Validation, validateEmail, validateNote} from "./validate"; import {Validation, validateEmail, validateNote, validateJSONInput} from "./validate";
import {CratesToJSON} from "./json_porter"; import {CratesToJSON, JSONToCrates} from "./json_porter";
const cards_to_pn_map = (cards) => { const cards_to_pn_map = (cards) => {
@ -38,26 +38,46 @@ const useLayout = ((set, get) => ({
isMobile: window.deviceIsMobile(), isMobile: window.deviceIsMobile(),
sideMenuIsOpen: false, sideMenuIsOpen: false,
newCardJustAdded: false, newCardJustAdded: false,
importIsOpen: false,
switchSideMenu: () => set(state => ({ switchSideMenu: () => set(state => ({
sideMenuIsOpen: !state.sideMenuIsOpen sideMenuIsOpen: !state.sideMenuIsOpen
})), })),
}));
const useImportJSON = ((set, get) => ({
importShouldOpen: false,
importValue: {
value: "",
error: Validation.OK
},
openImport: () => set(state => ({ openImport: () => set(state => ({
importIsOpen: true importShouldOpen: true
})), })),
closeImport: () => set(state => ({ closeImport: () => set(state => ({
importIsOpen: false importShouldOpen: false
})), })),
})) loadDescription: () => set(state => ({
importShouldOpen: false,
crates: JSONToCrates(state.importValue.value)
})),
updateImportDescription: (new_description) => set(state => ({
importValue: {
value: new_description,
error: validateJSONInput(new_description)
}
}))
}));
const useSubmitForm = ((set, get) => ({ const useSubmitForm = ((set, get) => ({
// TODO think about it // TODO think about it
isProcessing: false, isProcessing: false,
shouldShowRFQFeedback: true, shouldShowRFQFeedback: false,
RFQBodyType: 'email', processingResult: {
isProcessingComplete: true, status: Validation.OK,
message: ""
},
API_RFQ: shared_data.API_RFQ,
email: { email: {
value: "", value: "",
error: null error: null
@ -83,6 +103,17 @@ const useSubmitForm = ((set, get) => ({
} }
})), })),
_revalidateForm: () => set(state => ({
email: {
value: state.email.value,
error: validateEmail(state.email.value)
},
note: {
value: state.note.value,
error: validateEmail(state.note.value)
},
})),
updateDescription: () => set(state => ({ updateDescription: () => set(state => ({
description: CratesToJSON(state.crates) description: CratesToJSON(state.crates)
})), })),
@ -93,7 +124,41 @@ const useSubmitForm = ((set, get) => ({
closeDescription: () => set(state => ({ closeDescription: () => set(state => ({
shouldShowDescription: false shouldShowDescription: false
})), })),
submitForm: () => set(state => ({})) // TODO _submitForm: () => set(state => ({isProcessing: true})),
finishSubmitForm: (result) => set(state => ({
isProcessing: false,
shouldShowRFQFeedback: true,
processingResult: result})),
submitDisabled: () => (get().email.error !== Validation.OK),
submitForm: () => {
get().updateDescription();
get()._revalidateForm();
get()._submitForm();
if (get().submitDisabled()) return;
fetch(get().API_RFQ, {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
email: get().email.value,
note: get().note.value,
configuration: get().description
})
}).then(response => {
if (response.status !== 200) {
throw Error("Response status is not OK: " + response.status + ".\n" + response);
}
get().finishSubmitForm({status: Validation.OK, message: "We've received your request and will be in contact soon."})
}).catch(err => {
console.error("Request failed, reason:", err)
get().finishSubmitForm({
status: Validation.Invalid,
message: "We cannot receive your request. Try using the export by coping the configuration and send it to us at sales@m-labs.hk"
})
})
},
closeRFQFeedback: () => set(state => ({shouldShowRFQFeedback: false}))
})); }));
const useHighlighted = ((set, get) => ({ const useHighlighted = ((set, get) => ({
@ -263,7 +328,6 @@ const useCart = ((set, get) => ({
}, },
setCrateMode: (id, mode) => { setCrateMode: (id, mode) => {
console.log("setCrateMode", id, mode)
get()._setCrateMode(id, mode) get()._setCrateMode(id, mode)
get().fillWarnings(id); get().fillWarnings(id);
}, },
@ -292,8 +356,6 @@ const useCart = ((set, get) => ({
get()._updateOptions(crate_id, index, new_options); get()._updateOptions(crate_id, index, new_options);
get().fillWarnings(crate_id); get().fillWarnings(crate_id);
} }
// TODO load and save jsons?
})) }))
@ -304,4 +366,5 @@ export const useShopStore = create((...params) => ({
...useSubmitForm(...params), ...useSubmitForm(...params),
...useLayout(...params), ...useLayout(...params),
...useHighlighted(...params), ...useHighlighted(...params),
...useImportJSON(...params),
})) }))

View File

@ -20,8 +20,8 @@ module.exports = {
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'], extensions: ['.tsx', '.ts', '.js', '.jsx'],
}, },
devtool: "inline-source-map", //devtool: "inline-source-map",
mode: "development" //mode: "development"
//devtool: false, devtool: false,
//mode: "production" mode: "production"
}; };