@metron-labs/quote-react 0.1.34 → 0.1.36

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
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");
@@ -134,7 +173,12 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
134
173
  const backToResults = useCallback(() => {
135
174
  // Step back over the basket entry goToBasket pushed (popstate picks the screen), rather
136
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.
137
178
  if (window.location.hash === "#quote-request") {
179
+ if (leavingBasket.current)
180
+ return;
181
+ leavingBasket.current = true;
138
182
  window.history.back();
139
183
  return;
140
184
  }