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