@metron-labs/quote-react 0.1.33 → 0.1.35

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.
@@ -11,6 +11,7 @@ import BasketStage from "./stages/BasketStage";
11
11
  import { CartProvider, useCart } from "../lib/cart/CartContext";
12
12
  import { clientHeaders, readJson } from "../lib/api/boqClient";
13
13
  import { getPricingBreakdown } from "../util/pricing";
14
+ const SAVED_QUOTE_KEY = "metron-quick-quote";
14
15
  // Its own provider, separate from the estimating tools' - a quote in progress and a set
15
16
  // of calculator results are different baskets.
16
17
  export default function QuickQuote(props) {
@@ -24,7 +25,7 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
24
25
  const [processingStep, setProcessingStep] = useState(0);
25
26
  const [scheduleResult, setScheduleResult] = useState(null);
26
27
  const [scheduleReviewGateComplete, setScheduleReviewGateComplete] = useState(false);
27
- const { cart, addToCart, removeFromCart, updateCartQty, updateCartSellUnits, updateCartConversions, clearCart } = useCart();
28
+ const { cart, addToCart, addManyToCart, removeFromCart, updateCartQty, updateCartSellUnits, updateCartConversions, clearCart } = useCart();
28
29
  const [cartSeeded, setCartSeeded] = useState(false);
29
30
  const resolvedConversionInputs = useRef(new Map());
30
31
  const rootRef = useRef(null);
@@ -82,13 +83,49 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
82
83
  })
83
84
  .catch((error) => console.error("Catalogue conversion resolution failed", error));
84
85
  }, [cart, updateCartConversions]);
86
+ // The quote survives the widget restarting - the host re-rendering the page under us
87
+ // (Turbo restoring its own entry), or a refresh. It lives in this tab's sessionStorage and
88
+ // the URL hash says which step to show. Restored after mount, not in the initial state, so
89
+ // a server-rendered host doesn't hit a hydration mismatch.
90
+ const [restored, setRestored] = useState(false);
85
91
  useEffect(() => {
86
- if (window.location.hash !== "#matched-products" && window.location.hash !== "#quote-request")
87
- return;
88
- const url = new URL(window.location.href);
89
- url.hash = "";
90
- window.history.replaceState(window.history.state, "", url);
92
+ const hash = window.location.hash;
93
+ let saved = null;
94
+ try {
95
+ saved = JSON.parse(window.sessionStorage.getItem(SAVED_QUOTE_KEY) ?? "null");
96
+ }
97
+ catch { }
98
+ if (saved?.scheduleResult) {
99
+ setEntryMode("schedule");
100
+ setScheduleResult(saved.scheduleResult);
101
+ setScheduleReviewGateComplete(saved.reviewGateComplete);
102
+ setCartSeeded(saved.cartSeeded);
103
+ addManyToCart(Object.values(saved.cart));
104
+ if (hash === "#quote-request")
105
+ setStage("basket");
106
+ else if (hash === "#matched-products")
107
+ setStage("results");
108
+ }
109
+ else if (hash === "#matched-products" || hash === "#quote-request") {
110
+ const url = new URL(window.location.href);
111
+ url.hash = "";
112
+ window.history.replaceState(window.history.state, "", url);
113
+ }
114
+ setRestored(true);
91
115
  }, []);
116
+ useEffect(() => {
117
+ if (!restored)
118
+ return;
119
+ // ponytail: rewrites the whole quote on every basket edit; fine at schedule sizes,
120
+ // debounce it if large schedules make typing in quantities lag.
121
+ try {
122
+ if (!scheduleResult)
123
+ window.sessionStorage.removeItem(SAVED_QUOTE_KEY);
124
+ else
125
+ window.sessionStorage.setItem(SAVED_QUOTE_KEY, JSON.stringify({ scheduleResult, reviewGateComplete: scheduleReviewGateComplete, cartSeeded, cart }));
126
+ }
127
+ catch { }
128
+ }, [restored, scheduleResult, scheduleReviewGateComplete, cartSeeded, cart]);
92
129
  const writeQuoteHistory = useCallback((next) => {
93
130
  if (typeof window === "undefined")
94
131
  return;
@@ -102,8 +139,10 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
102
139
  const stale = next === "results" && (window.location.hash === "#matched-products" || window.location.hash === "#quote-request");
103
140
  window.history[stale ? "replaceState" : "pushState"]({ boqQuoteStage: next }, "", url);
104
141
  }, []);
142
+ const leavingBasket = useRef(false);
105
143
  useEffect(() => {
106
- const onPopState = () => {
144
+ const onPopState = (event) => {
145
+ leavingBasket.current = false;
107
146
  const hash = window.location.hash;
108
147
  if (hash === "#quote-request")
109
148
  setStage("basket");
@@ -112,14 +151,16 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
112
151
  else
113
152
  setStage("entry");
114
153
  // Turbo's popstate listener (registered before ours, so it can't be pre-empted) stamps its
115
- // restoration id over our entries. On the next back/forward it then treats them as its own
116
- // page visits and reloads the page, dropping the user on entry. Put our state back once
117
- // every listener has run.
154
+ // restoration id over any entry without one - ours, or the host's own entry for this page.
155
+ // On the next back/forward it then treats that entry as its page visit and re-renders the
156
+ // page, wiping the quote. Once every listener has run, undo the stamp.
157
+ const original = event.state;
158
+ const href = window.location.href;
118
159
  setTimeout(() => {
119
- const current = window.location.hash;
120
- if (current !== "#quote-request" && current !== "#matched-products")
160
+ if (original?.turbo || window.location.href !== href)
121
161
  return;
122
- window.history.replaceState({ boqQuoteStage: current === "#quote-request" ? "basket" : "results" }, "", window.location.href);
162
+ if (window.history.state?.turbo)
163
+ window.history.replaceState(original, "", href);
123
164
  });
124
165
  };
125
166
  window.addEventListener("popstate", onPopState);
@@ -132,7 +173,12 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
132
173
  const backToResults = useCallback(() => {
133
174
  // Step back over the basket entry goToBasket pushed (popstate picks the screen), rather
134
175
  // than rewriting it: each rewrite left an extra results entry the browser back had to cross.
176
+ // history.back() is async: the basket stays up until popstate, so a double click would
177
+ // otherwise step back twice - past results to the host's entry, which re-renders the page.
135
178
  if (window.location.hash === "#quote-request") {
179
+ if (leavingBasket.current)
180
+ return;
181
+ leavingBasket.current = true;
136
182
  window.history.back();
137
183
  return;
138
184
  }