@metron-labs/quote-react 0.1.56 → 0.1.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +31 -31
- package/README.md +253 -253
- package/dist/app/QuickQuote.js +16 -23
- package/dist/app/stages/EntryStage.js +4 -3
- package/dist/embed/quote.js +6972 -6972
- package/dist/lib/productImage.js +19 -19
- package/dist/lib/quoteHistory.d.ts +6 -0
- package/dist/lib/quoteHistory.js +87 -0
- package/dist/styles.css +6961 -6961
- package/package.json +53 -53
package/dist/app/QuickQuote.js
CHANGED
|
@@ -10,7 +10,12 @@ import { ENTRY_DRAFT_KEY } from "./stages/EntryStage";
|
|
|
10
10
|
import ScheduleResultsStage from "./stages/ScheduleResultsStage";
|
|
11
11
|
import BasketStage from "./stages/BasketStage";
|
|
12
12
|
import { CartProvider, useCart } from "../lib/cart/CartContext";
|
|
13
|
+
import { installQuoteHistory, setStep, stepBack } from "../lib/quoteHistory";
|
|
13
14
|
const SAVED_QUOTE_KEY = "metron-quick-quote";
|
|
15
|
+
// A tab left open for days shouldn't come back with old matches and old prices.
|
|
16
|
+
// ponytail: counts from the last edit, not from matching. Stamp the match time instead if
|
|
17
|
+
// prices move faster than people edit.
|
|
18
|
+
const SAVED_QUOTE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
14
19
|
// Its own provider, separate from the estimating tools' - a quote in progress and a set
|
|
15
20
|
// of calculator results are different baskets.
|
|
16
21
|
export default function QuickQuote(props) {
|
|
@@ -48,12 +53,17 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
|
|
|
48
53
|
// a server-rendered host doesn't hit a hydration mismatch.
|
|
49
54
|
const [restored, setRestored] = useState(false);
|
|
50
55
|
useEffect(() => {
|
|
56
|
+
installQuoteHistory();
|
|
51
57
|
const hash = window.location.hash;
|
|
52
58
|
let saved = null;
|
|
53
59
|
try {
|
|
54
60
|
saved = JSON.parse(window.sessionStorage.getItem(SAVED_QUOTE_KEY) ?? "null");
|
|
55
61
|
}
|
|
56
62
|
catch { }
|
|
63
|
+
// Older than the TTL (or saved before there was a stamp): start fresh. The save effect
|
|
64
|
+
// below then removes it, as scheduleResult stays null.
|
|
65
|
+
if (saved && !(Date.now() - (saved.savedAt ?? 0) < SAVED_QUOTE_TTL_MS))
|
|
66
|
+
saved = null;
|
|
57
67
|
if (saved?.scheduleResult) {
|
|
58
68
|
setEntryMode("schedule");
|
|
59
69
|
setScheduleResult(saved.scheduleResult);
|
|
@@ -68,7 +78,7 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
|
|
|
68
78
|
else if (hash === "#matched-products" || hash === "#quote-request") {
|
|
69
79
|
const url = new URL(window.location.href);
|
|
70
80
|
url.hash = "";
|
|
71
|
-
|
|
81
|
+
setStep("replaceState", window.history.state, url);
|
|
72
82
|
}
|
|
73
83
|
setRestored(true);
|
|
74
84
|
}, []);
|
|
@@ -81,7 +91,7 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
|
|
|
81
91
|
if (!scheduleResult)
|
|
82
92
|
window.sessionStorage.removeItem(SAVED_QUOTE_KEY);
|
|
83
93
|
else
|
|
84
|
-
window.sessionStorage.setItem(SAVED_QUOTE_KEY, JSON.stringify({ scheduleResult, reviewGateComplete: scheduleReviewGateComplete, cartSeeded, cart }));
|
|
94
|
+
window.sessionStorage.setItem(SAVED_QUOTE_KEY, JSON.stringify({ savedAt: Date.now(), scheduleResult, reviewGateComplete: scheduleReviewGateComplete, cartSeeded, cart }));
|
|
85
95
|
}
|
|
86
96
|
catch { }
|
|
87
97
|
}, [restored, scheduleResult, scheduleReviewGateComplete, cartSeeded, cart]);
|
|
@@ -96,11 +106,12 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
|
|
|
96
106
|
// New results while the URL still carries one of our steps (start over, or the host
|
|
97
107
|
// re-rendered the page under us) reuse that entry, or back would land on a stale copy.
|
|
98
108
|
const stale = next === "results" && (window.location.hash === "#matched-products" || window.location.hash === "#quote-request");
|
|
99
|
-
|
|
109
|
+
setStep(stale ? "replaceState" : "pushState", { boqQuoteStage: next }, url);
|
|
100
110
|
}, []);
|
|
101
111
|
const leavingBasket = useRef(false);
|
|
102
112
|
useEffect(() => {
|
|
103
|
-
|
|
113
|
+
// Host routers (Turbo, Salesfire) are kept out of these by lib/quoteHistory.
|
|
114
|
+
const onPopState = () => {
|
|
104
115
|
leavingBasket.current = false;
|
|
105
116
|
const hash = window.location.hash;
|
|
106
117
|
if (hash === "#quote-request")
|
|
@@ -112,24 +123,6 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
|
|
|
112
123
|
}
|
|
113
124
|
else
|
|
114
125
|
setStage("entry");
|
|
115
|
-
// Turbo's popstate listener (registered before ours, so it can't be pre-empted) stamps its
|
|
116
|
-
// restoration id over any entry without one - ours, or the host's own entry for this page.
|
|
117
|
-
// On the next back/forward it then treats that entry as its page visit and re-renders the
|
|
118
|
-
// page, wiping the quote. Once every listener has run, undo the stamp.
|
|
119
|
-
const original = event.state;
|
|
120
|
-
const href = window.location.href;
|
|
121
|
-
setTimeout(() => {
|
|
122
|
-
if (window.location.href !== href)
|
|
123
|
-
return;
|
|
124
|
-
// Our own steps always get our state back, even if a stamp slipped through earlier -
|
|
125
|
-
// otherwise it sticks and every later visit to that step is a Turbo page visit.
|
|
126
|
-
if (hash === "#quote-request" || hash === "#matched-products" || hash === "#quote-entry") {
|
|
127
|
-
window.history.replaceState({ boqQuoteStage: hash === "#quote-request" ? "basket" : hash === "#quote-entry" ? "entry" : "results" }, "", href);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (!original?.turbo && window.history.state?.turbo)
|
|
131
|
-
window.history.replaceState(original, "", href);
|
|
132
|
-
});
|
|
133
126
|
};
|
|
134
127
|
window.addEventListener("popstate", onPopState);
|
|
135
128
|
return () => window.removeEventListener("popstate", onPopState);
|
|
@@ -147,7 +140,7 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
|
|
|
147
140
|
if (leavingBasket.current)
|
|
148
141
|
return;
|
|
149
142
|
leavingBasket.current = true;
|
|
150
|
-
|
|
143
|
+
stepBack();
|
|
151
144
|
return;
|
|
152
145
|
}
|
|
153
146
|
setStage(scheduleResult ? "results" : "entry");
|
|
@@ -7,6 +7,7 @@ import CloseIcon from "../../components/CloseIcon";
|
|
|
7
7
|
import { FILE_EXTENSIONS, isPhotoFile, shrinkPhoto, validateLines, validateUploadFile } from "../../lib/boq/inputLimits";
|
|
8
8
|
import { ProcessingContent } from "./ProcessingStage";
|
|
9
9
|
import { useEscapeKey } from "../../lib/useEscapeKey";
|
|
10
|
+
import { setStep, stepBack } from "../../lib/quoteHistory";
|
|
10
11
|
const UPLOAD_ACCEPT = [...FILE_EXTENSIONS.schedule, ...FILE_EXTENSIONS.photo].join(",");
|
|
11
12
|
const STEPS = {
|
|
12
13
|
paste: [
|
|
@@ -67,7 +68,7 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
|
|
|
67
68
|
// the dialog would reuse this step and closing it would go back off the page.
|
|
68
69
|
const url = new URL(window.location.href);
|
|
69
70
|
url.hash = "";
|
|
70
|
-
|
|
71
|
+
setStep("replaceState", window.history.state, url);
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
lastTab.current = saved.tab;
|
|
@@ -110,7 +111,7 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
|
|
|
110
111
|
// Our own state only, as in QuickQuote's writeQuoteHistory. A results/basket hash left
|
|
111
112
|
// behind by "start over" is reused, or back would land on that stale step.
|
|
112
113
|
const stale = hash === "#matched-products" || hash === "#quote-request";
|
|
113
|
-
|
|
114
|
+
setStep(stale ? "replaceState" : "pushState", { boqQuoteStage: "entry" }, url);
|
|
114
115
|
};
|
|
115
116
|
const closeWorkflow = () => {
|
|
116
117
|
setActiveTab(null);
|
|
@@ -118,7 +119,7 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
|
|
|
118
119
|
// Drop the dialog's step too. Closing hides the dialog straight away, so a second
|
|
119
120
|
// Escape/click can't step back again past the page.
|
|
120
121
|
if (window.location.hash === DIALOG_HASH)
|
|
121
|
-
|
|
122
|
+
stepBack();
|
|
122
123
|
};
|
|
123
124
|
// Like the close button, but not mid-match, where the modal is showing progress.
|
|
124
125
|
useEscapeKey(Boolean(activeTab) && !matching, closeWorkflow);
|