@usethrottle/quotes 0.1.0

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/dist/index.cjs ADDED
@@ -0,0 +1,736 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.tsx
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ QuoteRequestForm: () => QuoteRequestForm,
24
+ QuoteView: () => QuoteView,
25
+ QuotesClient: () => QuotesClient,
26
+ ThrottleQuotesError: () => ThrottleQuotesError,
27
+ computeSelectionTotal: () => computeSelectionTotal,
28
+ createQuotesClient: () => createQuotesClient,
29
+ formatMoney: () => formatMoney,
30
+ scaleLineCharges: () => scaleLineCharges,
31
+ useQuote: () => useQuote,
32
+ useQuoteRequestForm: () => useQuoteRequestForm
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/errors.ts
37
+ var import_errors = require("@usethrottle/errors");
38
+ var ThrottleQuotesError = class extends import_errors.ThrottleError {
39
+ constructor(args) {
40
+ super(args);
41
+ this.name = "ThrottleQuotesError";
42
+ }
43
+ /** The offer moved on — reload and show the buyer the current revision. */
44
+ get isSuperseded() {
45
+ return this.code === "revision_superseded";
46
+ }
47
+ get isExpired() {
48
+ return this.code === "quote_expired";
49
+ }
50
+ /** Revision the server considers current, when it told us (spec §8.1). */
51
+ get currentRevisionId() {
52
+ const d = this.details;
53
+ return d?.currentRevisionId;
54
+ }
55
+ };
56
+
57
+ // src/client.ts
58
+ var DEFAULT_BASE = "https://api.usethrottle.dev";
59
+ var QuotesClient = class {
60
+ baseUrl;
61
+ fetchImpl;
62
+ timeoutMs;
63
+ constructor(opts = {}) {
64
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
65
+ if (!fetchImpl) throw new Error("QuotesClient: fetch is required");
66
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
67
+ this.fetchImpl = fetchImpl;
68
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
69
+ }
70
+ async request(method, path, body) {
71
+ const ctrl = new AbortController();
72
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
73
+ try {
74
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
75
+ method,
76
+ headers: body === void 0 ? {} : { "content-type": "application/json" },
77
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
78
+ signal: ctrl.signal
79
+ });
80
+ const json = await res.json().catch(() => ({}));
81
+ if (!res.ok) {
82
+ throw new ThrottleQuotesError({
83
+ code: json.error?.code ?? "unknown_error",
84
+ message: json.error?.message ?? `HTTP ${res.status}`,
85
+ statusCode: res.status,
86
+ details: json.error?.details
87
+ });
88
+ }
89
+ return json.data;
90
+ } finally {
91
+ clearTimeout(timer);
92
+ }
93
+ }
94
+ /** Fetch the quote a buyer link points at. 404s for unknown/rotated tokens. */
95
+ getQuote(token) {
96
+ return this.request("GET", `/api/v1/quote-links/${encodeURIComponent(token)}`);
97
+ }
98
+ /**
99
+ * Tell the merchant the buyer opened the quote. Fire-and-forget: the server
100
+ * throttles the resulting webhook to one per revision per 6h, and a failed
101
+ * beacon must never break the page.
102
+ */
103
+ async recordView(token) {
104
+ try {
105
+ await this.request("POST", `/api/v1/quote-links/${encodeURIComponent(token)}/view`, {});
106
+ } catch {
107
+ }
108
+ }
109
+ addComment(token, input) {
110
+ return this.request(
111
+ "POST",
112
+ `/api/v1/quote-links/${encodeURIComponent(token)}/comments`,
113
+ input
114
+ );
115
+ }
116
+ requestRevision(token, input = {}) {
117
+ return this.request(
118
+ "POST",
119
+ `/api/v1/quote-links/${encodeURIComponent(token)}/request-revision`,
120
+ input
121
+ );
122
+ }
123
+ decline(token, input = {}) {
124
+ return this.request(
125
+ "POST",
126
+ `/api/v1/quote-links/${encodeURIComponent(token)}/decline`,
127
+ input
128
+ );
129
+ }
130
+ /**
131
+ * Accept and get the checkout URL to send the buyer to.
132
+ *
133
+ * Re-accepting the same revision is idempotent and replays the same URL.
134
+ * Accepting a superseded revision throws `revision_superseded` carrying
135
+ * `currentRevisionId`; an expired offer throws `quote_expired`.
136
+ */
137
+ accept(token, input) {
138
+ return this.request(
139
+ "POST",
140
+ `/api/v1/quote-links/${encodeURIComponent(token)}/accept`,
141
+ input
142
+ );
143
+ }
144
+ /** URL of the PDF for the revision the buyer is looking at (server 302s to signed storage). */
145
+ pdfUrl(token) {
146
+ return `${this.baseUrl}/api/v1/quote-links/${encodeURIComponent(token)}/pdf`;
147
+ }
148
+ // ── RFQ intake ─────────────────────────────────────────────────────────────
149
+ getRequestForm(formToken) {
150
+ return this.request(
151
+ "GET",
152
+ `/api/v1/quote-request-forms/${encodeURIComponent(formToken)}/public`
153
+ );
154
+ }
155
+ /**
156
+ * Submit an RFQ. The response is deliberately thin (`{ submitted: true }`) —
157
+ * anonymous callers learn no internal identifiers. A submission caught by
158
+ * the honeypot returns the same shape without creating anything, so a bot
159
+ * cannot tell it was detected; treat success as "accepted", not "created".
160
+ */
161
+ submitRequest(formToken, input) {
162
+ return this.request("POST", "/api/v1/quote-requests", {
163
+ formToken,
164
+ ...input
165
+ });
166
+ }
167
+ };
168
+ function createQuotesClient(opts = {}) {
169
+ return new QuotesClient(opts);
170
+ }
171
+ function formatMoney(minorUnits, currency, locale) {
172
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(minorUnits / 100);
173
+ }
174
+ function scaleLineCharges(item, quantity) {
175
+ const base = item.quantity;
176
+ if (!base || base <= 0 || quantity === base) {
177
+ return { taxAmount: item.taxAmount, discountAmount: item.discountAmount };
178
+ }
179
+ const ratio = quantity / base;
180
+ return {
181
+ taxAmount: Math.round(item.taxAmount * ratio),
182
+ discountAmount: Math.round(item.discountAmount * ratio)
183
+ };
184
+ }
185
+ function computeSelectionTotal(items, selections, shippingTotal = 0) {
186
+ let total = shippingTotal;
187
+ for (const item of items) {
188
+ const state = selections[item.id];
189
+ if (item.optional && state && !state.selected) continue;
190
+ const qty = state?.quantity ?? item.quantity;
191
+ const { taxAmount, discountAmount } = scaleLineCharges(item, qty);
192
+ total += item.unitPrice * qty + taxAmount - discountAmount;
193
+ }
194
+ return total;
195
+ }
196
+
197
+ // src/hooks.ts
198
+ var import_react = require("react");
199
+ function useQuote(token, options = {}) {
200
+ const client = (0, import_react.useMemo)(
201
+ () => new QuotesClient(options),
202
+ // eslint-disable-next-line react-hooks/exhaustive-deps
203
+ [options.baseUrl, options.timeoutMs]
204
+ );
205
+ const [quote, setQuote] = (0, import_react.useState)(null);
206
+ const [loading, setLoading] = (0, import_react.useState)(true);
207
+ const [submitting, setSubmitting] = (0, import_react.useState)(false);
208
+ const [error, setError] = (0, import_react.useState)(null);
209
+ const [selections, setSelections] = (0, import_react.useState)({});
210
+ const beaconSent = (0, import_react.useRef)(false);
211
+ const load = (0, import_react.useCallback)(async () => {
212
+ setLoading(true);
213
+ try {
214
+ const q = await client.getQuote(token);
215
+ setQuote(q);
216
+ setError(null);
217
+ const next = {};
218
+ for (const item of q.currentRevision?.items ?? []) {
219
+ next[item.id] = { selected: item.optional ? item.optionalSelected : true, quantity: item.quantity };
220
+ }
221
+ setSelections(next);
222
+ } catch (e) {
223
+ setError(e);
224
+ } finally {
225
+ setLoading(false);
226
+ }
227
+ }, [client, token]);
228
+ (0, import_react.useEffect)(() => {
229
+ void load();
230
+ }, [load]);
231
+ (0, import_react.useEffect)(() => {
232
+ if (!quote || beaconSent.current) return;
233
+ beaconSent.current = true;
234
+ void client.recordView(token);
235
+ }, [client, quote, token]);
236
+ const setSelected = (0, import_react.useCallback)((itemId, selected) => {
237
+ setSelections((prev) => ({ ...prev, [itemId]: { ...prev[itemId] ?? { quantity: 1 }, selected } }));
238
+ }, []);
239
+ const setQuantity = (0, import_react.useCallback)((itemId, quantity) => {
240
+ setSelections((prev) => ({
241
+ ...prev,
242
+ [itemId]: { ...prev[itemId] ?? { selected: true }, quantity: Math.max(1, Math.floor(quantity) || 1) }
243
+ }));
244
+ }, []);
245
+ const total = (0, import_react.useMemo)(
246
+ () => computeSelectionTotal(
247
+ quote?.currentRevision?.items ?? [],
248
+ selections,
249
+ quote?.currentRevision?.shippingTotal ?? 0
250
+ ),
251
+ [quote, selections]
252
+ );
253
+ const buildSelections = (0, import_react.useCallback)(() => {
254
+ const items = quote?.currentRevision?.items ?? [];
255
+ const out = [];
256
+ for (const item of items) {
257
+ const state = selections[item.id];
258
+ if (!state) continue;
259
+ const sel = { itemId: item.id };
260
+ let include = false;
261
+ if (item.optional && state.selected !== item.optionalSelected) {
262
+ sel.selected = state.selected;
263
+ include = true;
264
+ }
265
+ if (item.qtyEditable && state.quantity !== item.quantity) {
266
+ sel.quantity = state.quantity;
267
+ include = true;
268
+ }
269
+ if (include) out.push(sel);
270
+ }
271
+ return out;
272
+ }, [quote, selections]);
273
+ const guard = (0, import_react.useCallback)(
274
+ async (fn) => {
275
+ setSubmitting(true);
276
+ try {
277
+ const result = await fn();
278
+ setError(null);
279
+ return result;
280
+ } catch (e) {
281
+ const err = e;
282
+ setError(err);
283
+ if (err.isSuperseded) void load();
284
+ throw err;
285
+ } finally {
286
+ setSubmitting(false);
287
+ }
288
+ },
289
+ [load]
290
+ );
291
+ const accept = (0, import_react.useCallback)(
292
+ async (input) => {
293
+ const revisionId = quote?.currentRevision?.id;
294
+ if (!revisionId) throw new Error("useQuote: no current revision to accept");
295
+ const itemSelections = buildSelections();
296
+ const res = await guard(
297
+ () => client.accept(token, {
298
+ ...input,
299
+ revisionId,
300
+ ...itemSelections.length ? { itemSelections } : {}
301
+ })
302
+ );
303
+ return { checkoutUrl: res.checkoutUrl };
304
+ },
305
+ [buildSelections, client, guard, quote, token]
306
+ );
307
+ const requestRevision = (0, import_react.useCallback)(
308
+ async (message, authorName) => {
309
+ const q = await guard(() => client.requestRevision(token, { message, authorName }));
310
+ setQuote(q);
311
+ },
312
+ [client, guard, token]
313
+ );
314
+ const decline = (0, import_react.useCallback)(
315
+ async (reason, authorName) => {
316
+ const q = await guard(() => client.decline(token, { reason, authorName }));
317
+ setQuote(q);
318
+ },
319
+ [client, guard, token]
320
+ );
321
+ const addComment = (0, import_react.useCallback)(
322
+ async (body, authorName) => {
323
+ await guard(() => client.addComment(token, { body, authorName }));
324
+ await load();
325
+ },
326
+ [client, guard, load, token]
327
+ );
328
+ return {
329
+ quote,
330
+ loading,
331
+ error,
332
+ refresh: load,
333
+ selections,
334
+ setSelected,
335
+ setQuantity,
336
+ total,
337
+ accept,
338
+ requestRevision,
339
+ decline,
340
+ addComment,
341
+ pdfUrl: client.pdfUrl(token),
342
+ submitting
343
+ };
344
+ }
345
+ function useQuoteRequestForm(formToken, options = {}) {
346
+ const client = (0, import_react.useMemo)(
347
+ () => new QuotesClient(options),
348
+ // eslint-disable-next-line react-hooks/exhaustive-deps
349
+ [options.baseUrl, options.timeoutMs]
350
+ );
351
+ const [config, setConfig] = (0, import_react.useState)(null);
352
+ const [loading, setLoading] = (0, import_react.useState)(true);
353
+ const [submitting, setSubmitting] = (0, import_react.useState)(false);
354
+ const [submitted, setSubmitted] = (0, import_react.useState)(false);
355
+ const [error, setError] = (0, import_react.useState)(null);
356
+ const formLoadedAt = (0, import_react.useRef)(Date.now()).current;
357
+ (0, import_react.useEffect)(() => {
358
+ let cancelled = false;
359
+ (async () => {
360
+ try {
361
+ const cfg = await client.getRequestForm(formToken);
362
+ if (!cancelled) setConfig(cfg);
363
+ } catch (e) {
364
+ if (!cancelled) setError(e);
365
+ } finally {
366
+ if (!cancelled) setLoading(false);
367
+ }
368
+ })();
369
+ return () => {
370
+ cancelled = true;
371
+ };
372
+ }, [client, formToken]);
373
+ const submit = (0, import_react.useCallback)(
374
+ async (input) => {
375
+ setSubmitting(true);
376
+ try {
377
+ await client.submitRequest(formToken, { formLoadedAt, ...input });
378
+ setSubmitted(true);
379
+ setError(null);
380
+ } catch (e) {
381
+ setError(e);
382
+ throw e;
383
+ } finally {
384
+ setSubmitting(false);
385
+ }
386
+ },
387
+ [client, formLoadedAt, formToken]
388
+ );
389
+ return { config, loading, error, submitting, submitted, submit, formLoadedAt };
390
+ }
391
+
392
+ // src/QuoteView.tsx
393
+ var import_react2 = require("react");
394
+ var import_jsx_runtime = require("react/jsx-runtime");
395
+ var row = { display: "flex", justifyContent: "space-between", gap: 16, padding: "8px 0" };
396
+ var muted = { color: "#666", fontSize: 13 };
397
+ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
398
+ const q = useQuote(token, options ?? {});
399
+ const [name, setName] = (0, import_react2.useState)("");
400
+ const [email, setEmail] = (0, import_react2.useState)("");
401
+ const [showAccept, setShowAccept] = (0, import_react2.useState)(false);
402
+ const [comment, setComment] = (0, import_react2.useState)("");
403
+ if (q.loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: loadingFallback ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Loading quote\u2026" }) });
404
+ if (!q.quote) {
405
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "This quote link is no longer available." });
406
+ }
407
+ const quote = q.quote;
408
+ const rev = quote.currentRevision;
409
+ const currency = quote.currency;
410
+ const ordered = quote.status === "ordered";
411
+ const canAct = quote.status === "ready";
412
+ const handleAccept = async () => {
413
+ const { checkoutUrl } = await q.accept({ acceptedByName: name, acceptedByEmail: email });
414
+ if (onCheckout) onCheckout(checkoutUrl);
415
+ else if (typeof window !== "undefined") window.location.href = checkoutUrl;
416
+ };
417
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, children: [
418
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { children: [
419
+ quote.branding?.displayName ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, children: quote.branding.displayName }) : null,
420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h1", { children: [
421
+ "Quote ",
422
+ quote.quoteNumber
423
+ ] }),
424
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, children: statusLabel(quote.status) }),
425
+ quote.buyerReference ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: muted, children: [
426
+ "Your reference: ",
427
+ quote.buyerReference
428
+ ] }) : null,
429
+ rev ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: muted, children: [
430
+ "Revision ",
431
+ rev.revisionNumber,
432
+ rev.expiresAt ? ` \xB7 valid until ${new Date(rev.expiresAt).toLocaleDateString()}` : null,
433
+ " \xB7 ",
434
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: q.pdfUrl, children: "Download PDF" })
435
+ ] }) : null
436
+ ] }),
437
+ q.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", children: q.error.isSuperseded ? "This quote was updated \u2014 showing the latest version." : q.error.message }) : null,
438
+ rev ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse" }, children: [
439
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
440
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { align: "left", children: "Item" }),
441
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { align: "right", children: "Unit price" }),
442
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { align: "right", children: "Qty" }),
443
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { align: "right", children: "Total" })
444
+ ] }) }),
445
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: rev.items.map((item) => {
446
+ const state = q.selections[item.id] ?? { selected: true, quantity: item.quantity };
447
+ const included = item.optional ? state.selected : true;
448
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
449
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { children: [
450
+ item.optional ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
451
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
452
+ "input",
453
+ {
454
+ type: "checkbox",
455
+ checked: state.selected,
456
+ disabled: !canAct,
457
+ onChange: (e) => q.setSelected(item.id, e.target.checked)
458
+ }
459
+ ),
460
+ " ",
461
+ item.name,
462
+ " ",
463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: muted, children: "optional" })
464
+ ] }) : item.name,
465
+ item.description ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: item.description }) : null
466
+ ] }),
467
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { align: "right", children: formatMoney(item.unitPrice, currency) }),
468
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { align: "right", children: item.qtyEditable && canAct ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
469
+ "input",
470
+ {
471
+ type: "number",
472
+ min: 1,
473
+ value: state.quantity,
474
+ disabled: !included,
475
+ "aria-label": `Quantity for ${item.name}`,
476
+ onChange: (e) => q.setQuantity(item.id, Number(e.target.value)),
477
+ style: { width: 64 }
478
+ }
479
+ ) : state.quantity }),
480
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { align: "right", children: included ? formatMoney(
481
+ item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,
482
+ currency
483
+ ) : formatMoney(0, currency) })
484
+ ] }, item.id);
485
+ }) })
486
+ ] }) : null,
487
+ rev ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
488
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: row, children: [
489
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Total" }),
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: formatMoney(q.total, currency) })
491
+ ] }),
492
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, children: termsLabel(rev.paymentTerms, rev.depositAmount, currency) }),
493
+ rev.leadTime ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: muted, children: [
494
+ "Lead time: ",
495
+ rev.leadTime
496
+ ] }) : null,
497
+ rev.buyerNote ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("blockquote", { children: rev.buyerNote }) : null
498
+ ] }) : null,
499
+ ordered ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
500
+ "Ordered",
501
+ quote.orderId ? ` \u2014 order ${quote.orderId}` : "",
502
+ ". Thank you."
503
+ ] }) : canAct ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
504
+ showAccept ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
505
+ "form",
506
+ {
507
+ onSubmit: (e) => {
508
+ e.preventDefault();
509
+ void handleAccept();
510
+ },
511
+ children: [
512
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
513
+ "Your name",
514
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { value: name, onChange: (e) => setName(e.target.value), required: true })
515
+ ] }),
516
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
517
+ "Email",
518
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), required: true })
519
+ ] }),
520
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: q.submitting || !name || !email, children: "Confirm & continue to payment" })
521
+ ]
522
+ }
523
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => setShowAccept(true), children: "Accept & checkout" }),
524
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
525
+ "button",
526
+ {
527
+ type: "button",
528
+ disabled: q.submitting,
529
+ onClick: () => void q.requestRevision("Please revise this quote.", name || void 0),
530
+ children: "Request changes"
531
+ }
532
+ ),
533
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: q.submitting, onClick: () => void q.decline(void 0, name || void 0), children: "Decline" })
534
+ ] }) : null,
535
+ quote.revisionHistory.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { children: [
536
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
537
+ "Previous versions (",
538
+ quote.revisionHistory.length,
539
+ ")"
540
+ ] }),
541
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: quote.revisionHistory.map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
542
+ "Revision ",
543
+ r.revisionNumber,
544
+ " \u2014 ",
545
+ formatMoney(r.total, currency),
546
+ r.changeSummary ? ` \xB7 ${r.changeSummary}` : ""
547
+ ] }, r.id)) })
548
+ ] }) : null,
549
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
550
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "Questions" }),
551
+ quote.comments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
552
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
553
+ c.authorName ?? (c.authorType === "merchant" ? "Seller" : "You"),
554
+ ":"
555
+ ] }),
556
+ " ",
557
+ c.body
558
+ ] }, c.id)),
559
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
560
+ "form",
561
+ {
562
+ onSubmit: (e) => {
563
+ e.preventDefault();
564
+ const body = comment.trim();
565
+ if (!body) return;
566
+ setComment("");
567
+ void q.addComment(body, name || void 0);
568
+ },
569
+ children: [
570
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
571
+ "input",
572
+ {
573
+ value: comment,
574
+ onChange: (e) => setComment(e.target.value),
575
+ placeholder: "Ask a question\u2026",
576
+ "aria-label": "Ask a question"
577
+ }
578
+ ),
579
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: q.submitting || !comment.trim(), children: "Send" })
580
+ ]
581
+ }
582
+ )
583
+ ] })
584
+ ] });
585
+ }
586
+ function statusLabel(status) {
587
+ switch (status) {
588
+ case "in_progress":
589
+ return "We are preparing your quote";
590
+ case "ready":
591
+ return "Ready for your review";
592
+ case "being_revised":
593
+ return "We are revising this quote";
594
+ case "accepted":
595
+ return "Accepted \u2014 completing payment";
596
+ case "ordered":
597
+ return "Ordered";
598
+ case "declined":
599
+ return "Declined";
600
+ case "expired":
601
+ return "This quote has expired \u2014 request updated pricing";
602
+ default:
603
+ return status;
604
+ }
605
+ }
606
+ function termsLabel(terms, depositAmount, currency) {
607
+ if (terms.mode === "net_terms") return `Payment terms: Net ${terms.netN}`;
608
+ if (terms.mode === "deposit_balance") {
609
+ const dep = depositAmount != null ? formatMoney(depositAmount, currency) : "a deposit";
610
+ return `${dep} due today, balance Net ${terms.balanceNetN}`;
611
+ }
612
+ return "Payment in full";
613
+ }
614
+
615
+ // src/QuoteRequestForm.tsx
616
+ var import_react3 = require("react");
617
+ var import_jsx_runtime2 = require("react/jsx-runtime");
618
+ var honeypotStyle = {
619
+ position: "absolute",
620
+ width: 1,
621
+ height: 1,
622
+ padding: 0,
623
+ margin: -1,
624
+ overflow: "hidden",
625
+ clip: "rect(0 0 0 0)",
626
+ whiteSpace: "nowrap",
627
+ border: 0
628
+ };
629
+ function QuoteRequestForm({
630
+ formToken,
631
+ options,
632
+ turnstileToken,
633
+ onSubmitted,
634
+ loadingFallback,
635
+ className
636
+ }) {
637
+ const f = useQuoteRequestForm(formToken, options ?? {});
638
+ const [values, setValues] = (0, import_react3.useState)({
639
+ email: "",
640
+ name: "",
641
+ companyName: "",
642
+ phone: "",
643
+ buyerReference: "",
644
+ message: "",
645
+ website: ""
646
+ // honeypot
647
+ });
648
+ const set = (k) => (e) => setValues((prev) => ({ ...prev, [k]: e.target.value }));
649
+ if (f.loading) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: loadingFallback ?? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Loading\u2026" }) });
650
+ if (!f.config) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "This request form is not available." });
651
+ if (f.submitted) {
652
+ const redirect = f.config.redirectUrl;
653
+ if (redirect && /^https?:\/\//i.test(redirect) && typeof window !== "undefined") {
654
+ window.location.href = redirect;
655
+ }
656
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: f.config.successMessage ?? "Thanks \u2014 we will be in touch shortly." });
657
+ }
658
+ const fields = f.config.fields ?? {};
659
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
660
+ "form",
661
+ {
662
+ className,
663
+ onSubmit: (e) => {
664
+ e.preventDefault();
665
+ void f.submit({
666
+ email: values.email,
667
+ message: values.message,
668
+ name: values.name || null,
669
+ companyName: fields.companyName ? values.companyName || null : null,
670
+ phone: fields.phone ? values.phone || null : null,
671
+ buyerReference: fields.buyerReference ? values.buyerReference || null : null,
672
+ website: values.website || null,
673
+ ...turnstileToken ? { turnstileToken } : {}
674
+ }).then(() => onSubmitted?.()).catch(() => {
675
+ });
676
+ },
677
+ children: [
678
+ f.config.title ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { children: f.config.title }) : null,
679
+ f.config.introText ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: f.config.introText }) : null,
680
+ f.error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "alert", children: f.error.message }) : null,
681
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
682
+ "Email",
683
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "email", value: values.email, onChange: set("email"), required: true })
684
+ ] }),
685
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
686
+ "Name",
687
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: values.name, onChange: set("name") })
688
+ ] }),
689
+ fields.companyName ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
690
+ "Company",
691
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: values.companyName, onChange: set("companyName") })
692
+ ] }) : null,
693
+ fields.phone ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
694
+ "Phone",
695
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: values.phone, onChange: set("phone") })
696
+ ] }) : null,
697
+ fields.buyerReference ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
698
+ "Your reference #",
699
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: values.buyerReference, onChange: set("buyerReference") })
700
+ ] }) : null,
701
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
702
+ "What do you need?",
703
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("textarea", { value: values.message, onChange: set("message"), required: true })
704
+ ] }),
705
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: honeypotStyle, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
706
+ "Website",
707
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
708
+ "input",
709
+ {
710
+ name: "website",
711
+ tabIndex: -1,
712
+ autoComplete: "off",
713
+ value: values.website,
714
+ onChange: set("website")
715
+ }
716
+ )
717
+ ] }) }),
718
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "submit", disabled: f.submitting || !values.email || !values.message, children: f.submitting ? "Sending\u2026" : "Request a quote" })
719
+ ]
720
+ }
721
+ );
722
+ }
723
+ // Annotate the CommonJS export names for ESM import in node:
724
+ 0 && (module.exports = {
725
+ QuoteRequestForm,
726
+ QuoteView,
727
+ QuotesClient,
728
+ ThrottleQuotesError,
729
+ computeSelectionTotal,
730
+ createQuotesClient,
731
+ formatMoney,
732
+ scaleLineCharges,
733
+ useQuote,
734
+ useQuoteRequestForm
735
+ });
736
+ //# sourceMappingURL=index.cjs.map