@usethrottle/quotes 0.1.0 → 0.2.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/README.md +38 -0
- package/dist/index.cjs +176 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -3
- package/dist/index.d.ts +109 -3
- package/dist/index.js +174 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -108,13 +108,36 @@ interface RequestFormConfig {
|
|
|
108
108
|
companyName?: boolean;
|
|
109
109
|
phone?: boolean;
|
|
110
110
|
buyerReference?: boolean;
|
|
111
|
+
lineItems?: boolean;
|
|
112
|
+
targetPrice?: boolean;
|
|
111
113
|
requireTurnstile?: boolean;
|
|
112
114
|
};
|
|
113
115
|
branding?: QuoteBranding | null;
|
|
114
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A line the buyer wants quoted.
|
|
119
|
+
*
|
|
120
|
+
* `targetUnitPrice` (minor units) is what they are hoping to pay. It is
|
|
121
|
+
* recorded as part of the request and shown to the rep; it never becomes the
|
|
122
|
+
* quoted price. Nothing sent from a browser can set what you charge.
|
|
123
|
+
*/
|
|
124
|
+
interface QuoteRequestItem {
|
|
125
|
+
name: string;
|
|
126
|
+
sku?: string | null;
|
|
127
|
+
/** Your own product id, echoed back on the quote line. */
|
|
128
|
+
referenceId?: string | null;
|
|
129
|
+
quantity?: number;
|
|
130
|
+
/** The buyer's spec for this line — size, finish, delivery date. */
|
|
131
|
+
note?: string | null;
|
|
132
|
+
targetUnitPrice?: number | null;
|
|
133
|
+
imageUrl?: string | null;
|
|
134
|
+
}
|
|
115
135
|
interface SubmitQuoteRequestInput {
|
|
116
136
|
email: string;
|
|
117
|
-
|
|
137
|
+
/** Free text. Optional when `items` carries at least one line. */
|
|
138
|
+
message?: string | null;
|
|
139
|
+
/** Lines to quote. Optional when `message` is written. */
|
|
140
|
+
items?: QuoteRequestItem[];
|
|
118
141
|
name?: string | null;
|
|
119
142
|
companyName?: string | null;
|
|
120
143
|
phone?: string | null;
|
|
@@ -257,6 +280,61 @@ declare class ThrottleQuotesError extends ThrottleError {
|
|
|
257
280
|
get currentRevisionId(): string | undefined;
|
|
258
281
|
}
|
|
259
282
|
|
|
283
|
+
/**
|
|
284
|
+
* A quote cart: the buyer's running list of things to be quoted, built up
|
|
285
|
+
* while they browse.
|
|
286
|
+
*
|
|
287
|
+
* Throttle stores no catalog, so the storefront owns product data and hands
|
|
288
|
+
* lines in — the same arrangement as the shopping cart. Nothing here is sent
|
|
289
|
+
* until `read()` is submitted through `QuotesClient.submitRequest`, and the
|
|
290
|
+
* amounts a storefront attaches are the buyer's TARGET price, never a price
|
|
291
|
+
* we will honour.
|
|
292
|
+
*
|
|
293
|
+
* Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product
|
|
294
|
+
* twice increases its quantity instead of stacking duplicate rows.
|
|
295
|
+
*/
|
|
296
|
+
interface QuoteCartLine extends QuoteRequestItem {
|
|
297
|
+
/** Stable identity for updates and removals. */
|
|
298
|
+
key: string;
|
|
299
|
+
quantity: number;
|
|
300
|
+
}
|
|
301
|
+
interface QuoteCartStorage {
|
|
302
|
+
getItem(key: string): string | null;
|
|
303
|
+
setItem(key: string, value: string): void;
|
|
304
|
+
removeItem(key: string): void;
|
|
305
|
+
}
|
|
306
|
+
interface QuoteCartOptions {
|
|
307
|
+
/** Namespacing key, so two storefronts on one origin cannot collide. */
|
|
308
|
+
storageKey?: string;
|
|
309
|
+
/**
|
|
310
|
+
* Where the cart survives page navigation. Defaults to `localStorage` in a
|
|
311
|
+
* browser and to memory everywhere else (SSR, tests) — a cart that throws
|
|
312
|
+
* during server rendering would take the whole page down.
|
|
313
|
+
*/
|
|
314
|
+
storage?: QuoteCartStorage | null;
|
|
315
|
+
/** Refuse to grow past this many distinct lines. The API caps at 100. */
|
|
316
|
+
maxLines?: number;
|
|
317
|
+
}
|
|
318
|
+
interface QuoteCart {
|
|
319
|
+
/** Current lines, oldest first. */
|
|
320
|
+
read(): QuoteCartLine[];
|
|
321
|
+
/** Total quantity across lines — what a header badge should show. */
|
|
322
|
+
count(): number;
|
|
323
|
+
/** Add a line, merging quantities into an existing one with the same key. */
|
|
324
|
+
add(item: QuoteRequestItem): QuoteCartLine[];
|
|
325
|
+
/** Add several at once (a "quote this whole cart" button). */
|
|
326
|
+
addMany(items: QuoteRequestItem[]): QuoteCartLine[];
|
|
327
|
+
/** Change any field of a line. A quantity below 1 removes it. */
|
|
328
|
+
update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];
|
|
329
|
+
remove(key: string): QuoteCartLine[];
|
|
330
|
+
clear(): void;
|
|
331
|
+
/** Subscribe to every change. Returns an unsubscribe function. */
|
|
332
|
+
subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;
|
|
333
|
+
/** The `items` payload for `QuotesClient.submitRequest`. */
|
|
334
|
+
toRequestItems(): QuoteRequestItem[];
|
|
335
|
+
}
|
|
336
|
+
declare function createQuoteCart(options?: QuoteCartOptions): QuoteCart;
|
|
337
|
+
|
|
260
338
|
interface UseQuoteResult {
|
|
261
339
|
quote: BuyerQuote | null;
|
|
262
340
|
loading: boolean;
|
|
@@ -302,6 +380,28 @@ interface UseQuoteRequestFormResult {
|
|
|
302
380
|
/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */
|
|
303
381
|
declare function useQuoteRequestForm(formToken: string, options?: QuotesClientOptions): UseQuoteRequestFormResult;
|
|
304
382
|
|
|
383
|
+
interface UseQuoteCartResult {
|
|
384
|
+
lines: QuoteCartLine[];
|
|
385
|
+
/** Total quantity across lines — for a header badge. */
|
|
386
|
+
count: number;
|
|
387
|
+
add: (item: QuoteRequestItem) => void;
|
|
388
|
+
addMany: (items: QuoteRequestItem[]) => void;
|
|
389
|
+
update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;
|
|
390
|
+
remove: (key: string) => void;
|
|
391
|
+
clear: () => void;
|
|
392
|
+
/** The underlying cart, for use outside React (an Add to Quote button in plain JS). */
|
|
393
|
+
cart: QuoteCart;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* A quote cart the buyer fills while browsing, then submits as one request.
|
|
397
|
+
*
|
|
398
|
+
* The cart persists across page views, so an Add to Quote button on a product
|
|
399
|
+
* page and a Request quote page elsewhere on the site see the same lines.
|
|
400
|
+
* Pass the result of `toRequestItems()` (or `lines`) to
|
|
401
|
+
* `useQuoteRequestForm().submit` as `items`.
|
|
402
|
+
*/
|
|
403
|
+
declare function useQuoteCart(options?: QuoteCartOptions): UseQuoteCartResult;
|
|
404
|
+
|
|
305
405
|
interface QuoteViewProps {
|
|
306
406
|
/** The `qtl_…` token from the buyer's link. */
|
|
307
407
|
token: string;
|
|
@@ -337,6 +437,12 @@ interface QuoteRequestFormProps {
|
|
|
337
437
|
* API fails the submission closed.
|
|
338
438
|
*/
|
|
339
439
|
turnstileToken?: string;
|
|
440
|
+
/**
|
|
441
|
+
* Lines to submit with the request — typically `useQuoteCart().lines` after
|
|
442
|
+
* the buyer has been adding products while browsing. When present, the
|
|
443
|
+
* written message is optional.
|
|
444
|
+
*/
|
|
445
|
+
items?: QuoteRequestItem[];
|
|
340
446
|
onSubmitted?: () => void;
|
|
341
447
|
loadingFallback?: ReactNode;
|
|
342
448
|
className?: string;
|
|
@@ -349,6 +455,6 @@ interface QuoteRequestFormProps {
|
|
|
349
455
|
* a success-shaped response and creates nothing, so `onSubmitted` fires either
|
|
350
456
|
* way — by design, a bot must not learn it was caught.
|
|
351
457
|
*/
|
|
352
|
-
declare function QuoteRequestForm({ formToken, options, turnstileToken, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
|
|
458
|
+
declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
|
|
353
459
|
|
|
354
|
-
export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteRequestForm };
|
|
460
|
+
export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
|
package/dist/index.d.ts
CHANGED
|
@@ -108,13 +108,36 @@ interface RequestFormConfig {
|
|
|
108
108
|
companyName?: boolean;
|
|
109
109
|
phone?: boolean;
|
|
110
110
|
buyerReference?: boolean;
|
|
111
|
+
lineItems?: boolean;
|
|
112
|
+
targetPrice?: boolean;
|
|
111
113
|
requireTurnstile?: boolean;
|
|
112
114
|
};
|
|
113
115
|
branding?: QuoteBranding | null;
|
|
114
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A line the buyer wants quoted.
|
|
119
|
+
*
|
|
120
|
+
* `targetUnitPrice` (minor units) is what they are hoping to pay. It is
|
|
121
|
+
* recorded as part of the request and shown to the rep; it never becomes the
|
|
122
|
+
* quoted price. Nothing sent from a browser can set what you charge.
|
|
123
|
+
*/
|
|
124
|
+
interface QuoteRequestItem {
|
|
125
|
+
name: string;
|
|
126
|
+
sku?: string | null;
|
|
127
|
+
/** Your own product id, echoed back on the quote line. */
|
|
128
|
+
referenceId?: string | null;
|
|
129
|
+
quantity?: number;
|
|
130
|
+
/** The buyer's spec for this line — size, finish, delivery date. */
|
|
131
|
+
note?: string | null;
|
|
132
|
+
targetUnitPrice?: number | null;
|
|
133
|
+
imageUrl?: string | null;
|
|
134
|
+
}
|
|
115
135
|
interface SubmitQuoteRequestInput {
|
|
116
136
|
email: string;
|
|
117
|
-
|
|
137
|
+
/** Free text. Optional when `items` carries at least one line. */
|
|
138
|
+
message?: string | null;
|
|
139
|
+
/** Lines to quote. Optional when `message` is written. */
|
|
140
|
+
items?: QuoteRequestItem[];
|
|
118
141
|
name?: string | null;
|
|
119
142
|
companyName?: string | null;
|
|
120
143
|
phone?: string | null;
|
|
@@ -257,6 +280,61 @@ declare class ThrottleQuotesError extends ThrottleError {
|
|
|
257
280
|
get currentRevisionId(): string | undefined;
|
|
258
281
|
}
|
|
259
282
|
|
|
283
|
+
/**
|
|
284
|
+
* A quote cart: the buyer's running list of things to be quoted, built up
|
|
285
|
+
* while they browse.
|
|
286
|
+
*
|
|
287
|
+
* Throttle stores no catalog, so the storefront owns product data and hands
|
|
288
|
+
* lines in — the same arrangement as the shopping cart. Nothing here is sent
|
|
289
|
+
* until `read()` is submitted through `QuotesClient.submitRequest`, and the
|
|
290
|
+
* amounts a storefront attaches are the buyer's TARGET price, never a price
|
|
291
|
+
* we will honour.
|
|
292
|
+
*
|
|
293
|
+
* Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product
|
|
294
|
+
* twice increases its quantity instead of stacking duplicate rows.
|
|
295
|
+
*/
|
|
296
|
+
interface QuoteCartLine extends QuoteRequestItem {
|
|
297
|
+
/** Stable identity for updates and removals. */
|
|
298
|
+
key: string;
|
|
299
|
+
quantity: number;
|
|
300
|
+
}
|
|
301
|
+
interface QuoteCartStorage {
|
|
302
|
+
getItem(key: string): string | null;
|
|
303
|
+
setItem(key: string, value: string): void;
|
|
304
|
+
removeItem(key: string): void;
|
|
305
|
+
}
|
|
306
|
+
interface QuoteCartOptions {
|
|
307
|
+
/** Namespacing key, so two storefronts on one origin cannot collide. */
|
|
308
|
+
storageKey?: string;
|
|
309
|
+
/**
|
|
310
|
+
* Where the cart survives page navigation. Defaults to `localStorage` in a
|
|
311
|
+
* browser and to memory everywhere else (SSR, tests) — a cart that throws
|
|
312
|
+
* during server rendering would take the whole page down.
|
|
313
|
+
*/
|
|
314
|
+
storage?: QuoteCartStorage | null;
|
|
315
|
+
/** Refuse to grow past this many distinct lines. The API caps at 100. */
|
|
316
|
+
maxLines?: number;
|
|
317
|
+
}
|
|
318
|
+
interface QuoteCart {
|
|
319
|
+
/** Current lines, oldest first. */
|
|
320
|
+
read(): QuoteCartLine[];
|
|
321
|
+
/** Total quantity across lines — what a header badge should show. */
|
|
322
|
+
count(): number;
|
|
323
|
+
/** Add a line, merging quantities into an existing one with the same key. */
|
|
324
|
+
add(item: QuoteRequestItem): QuoteCartLine[];
|
|
325
|
+
/** Add several at once (a "quote this whole cart" button). */
|
|
326
|
+
addMany(items: QuoteRequestItem[]): QuoteCartLine[];
|
|
327
|
+
/** Change any field of a line. A quantity below 1 removes it. */
|
|
328
|
+
update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];
|
|
329
|
+
remove(key: string): QuoteCartLine[];
|
|
330
|
+
clear(): void;
|
|
331
|
+
/** Subscribe to every change. Returns an unsubscribe function. */
|
|
332
|
+
subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;
|
|
333
|
+
/** The `items` payload for `QuotesClient.submitRequest`. */
|
|
334
|
+
toRequestItems(): QuoteRequestItem[];
|
|
335
|
+
}
|
|
336
|
+
declare function createQuoteCart(options?: QuoteCartOptions): QuoteCart;
|
|
337
|
+
|
|
260
338
|
interface UseQuoteResult {
|
|
261
339
|
quote: BuyerQuote | null;
|
|
262
340
|
loading: boolean;
|
|
@@ -302,6 +380,28 @@ interface UseQuoteRequestFormResult {
|
|
|
302
380
|
/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */
|
|
303
381
|
declare function useQuoteRequestForm(formToken: string, options?: QuotesClientOptions): UseQuoteRequestFormResult;
|
|
304
382
|
|
|
383
|
+
interface UseQuoteCartResult {
|
|
384
|
+
lines: QuoteCartLine[];
|
|
385
|
+
/** Total quantity across lines — for a header badge. */
|
|
386
|
+
count: number;
|
|
387
|
+
add: (item: QuoteRequestItem) => void;
|
|
388
|
+
addMany: (items: QuoteRequestItem[]) => void;
|
|
389
|
+
update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;
|
|
390
|
+
remove: (key: string) => void;
|
|
391
|
+
clear: () => void;
|
|
392
|
+
/** The underlying cart, for use outside React (an Add to Quote button in plain JS). */
|
|
393
|
+
cart: QuoteCart;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* A quote cart the buyer fills while browsing, then submits as one request.
|
|
397
|
+
*
|
|
398
|
+
* The cart persists across page views, so an Add to Quote button on a product
|
|
399
|
+
* page and a Request quote page elsewhere on the site see the same lines.
|
|
400
|
+
* Pass the result of `toRequestItems()` (or `lines`) to
|
|
401
|
+
* `useQuoteRequestForm().submit` as `items`.
|
|
402
|
+
*/
|
|
403
|
+
declare function useQuoteCart(options?: QuoteCartOptions): UseQuoteCartResult;
|
|
404
|
+
|
|
305
405
|
interface QuoteViewProps {
|
|
306
406
|
/** The `qtl_…` token from the buyer's link. */
|
|
307
407
|
token: string;
|
|
@@ -337,6 +437,12 @@ interface QuoteRequestFormProps {
|
|
|
337
437
|
* API fails the submission closed.
|
|
338
438
|
*/
|
|
339
439
|
turnstileToken?: string;
|
|
440
|
+
/**
|
|
441
|
+
* Lines to submit with the request — typically `useQuoteCart().lines` after
|
|
442
|
+
* the buyer has been adding products while browsing. When present, the
|
|
443
|
+
* written message is optional.
|
|
444
|
+
*/
|
|
445
|
+
items?: QuoteRequestItem[];
|
|
340
446
|
onSubmitted?: () => void;
|
|
341
447
|
loadingFallback?: ReactNode;
|
|
342
448
|
className?: string;
|
|
@@ -349,6 +455,6 @@ interface QuoteRequestFormProps {
|
|
|
349
455
|
* a success-shaped response and creates nothing, so `onSubmitted` fires either
|
|
350
456
|
* way — by design, a bot must not learn it was caught.
|
|
351
457
|
*/
|
|
352
|
-
declare function QuoteRequestForm({ formToken, options, turnstileToken, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
|
|
458
|
+
declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
|
|
353
459
|
|
|
354
|
-
export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteRequestForm };
|
|
460
|
+
export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
|
package/dist/index.js
CHANGED
|
@@ -161,6 +161,125 @@ function computeSelectionTotal(items, selections, shippingTotal = 0) {
|
|
|
161
161
|
|
|
162
162
|
// src/hooks.ts
|
|
163
163
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
164
|
+
|
|
165
|
+
// src/quote-cart.ts
|
|
166
|
+
var DEFAULT_KEY = "throttle:quote-cart";
|
|
167
|
+
var DEFAULT_MAX_LINES = 100;
|
|
168
|
+
function memoryStorage() {
|
|
169
|
+
const map = /* @__PURE__ */ new Map();
|
|
170
|
+
return {
|
|
171
|
+
getItem: (key) => map.get(key) ?? null,
|
|
172
|
+
setItem: (key, value) => void map.set(key, value),
|
|
173
|
+
removeItem: (key) => void map.delete(key)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function defaultStorage() {
|
|
177
|
+
try {
|
|
178
|
+
if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
179
|
+
return globalThis.localStorage;
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
return memoryStorage();
|
|
184
|
+
}
|
|
185
|
+
function lineKey(item) {
|
|
186
|
+
const raw = item.sku?.trim() || item.referenceId?.trim() || item.name.trim();
|
|
187
|
+
return raw.toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
function normalizeQuantity(value) {
|
|
190
|
+
const n = Math.floor(Number(value ?? 1));
|
|
191
|
+
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
192
|
+
}
|
|
193
|
+
function createQuoteCart(options = {}) {
|
|
194
|
+
const storageKey = options.storageKey ?? DEFAULT_KEY;
|
|
195
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
196
|
+
const storage = options.storage === null ? memoryStorage() : options.storage ?? defaultStorage();
|
|
197
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
198
|
+
function load() {
|
|
199
|
+
try {
|
|
200
|
+
const raw = storage.getItem(storageKey);
|
|
201
|
+
if (!raw) return [];
|
|
202
|
+
const parsed = JSON.parse(raw);
|
|
203
|
+
if (!Array.isArray(parsed)) return [];
|
|
204
|
+
return parsed.filter((line) => line && typeof line.name === "string" && line.name.trim()).map((line) => ({
|
|
205
|
+
...line,
|
|
206
|
+
key: typeof line.key === "string" && line.key ? line.key : lineKey(line),
|
|
207
|
+
quantity: normalizeQuantity(line.quantity)
|
|
208
|
+
})).slice(0, maxLines);
|
|
209
|
+
} catch {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function save(lines) {
|
|
214
|
+
try {
|
|
215
|
+
storage.setItem(storageKey, JSON.stringify(lines));
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
for (const listener of listeners) listener(lines);
|
|
219
|
+
return lines;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
read: load,
|
|
223
|
+
count() {
|
|
224
|
+
return load().reduce((sum, line) => sum + line.quantity, 0);
|
|
225
|
+
},
|
|
226
|
+
add(item) {
|
|
227
|
+
return this.addMany([item]);
|
|
228
|
+
},
|
|
229
|
+
addMany(items) {
|
|
230
|
+
const lines = load();
|
|
231
|
+
for (const item of items) {
|
|
232
|
+
if (!item?.name?.trim()) continue;
|
|
233
|
+
const key = lineKey(item);
|
|
234
|
+
const existing = lines.find((line) => line.key === key);
|
|
235
|
+
if (existing) {
|
|
236
|
+
existing.quantity += normalizeQuantity(item.quantity);
|
|
237
|
+
if (item.note) existing.note = item.note;
|
|
238
|
+
if (item.targetUnitPrice != null) existing.targetUnitPrice = item.targetUnitPrice;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (lines.length >= maxLines) break;
|
|
242
|
+
lines.push({ ...item, key, quantity: normalizeQuantity(item.quantity) });
|
|
243
|
+
}
|
|
244
|
+
return save(lines);
|
|
245
|
+
},
|
|
246
|
+
update(key, patch) {
|
|
247
|
+
const lines = load();
|
|
248
|
+
const index = lines.findIndex((line) => line.key === key);
|
|
249
|
+
if (index === -1) return lines;
|
|
250
|
+
if (patch.quantity != null && Math.floor(Number(patch.quantity)) < 1) {
|
|
251
|
+
lines.splice(index, 1);
|
|
252
|
+
return save(lines);
|
|
253
|
+
}
|
|
254
|
+
lines[index] = {
|
|
255
|
+
...lines[index],
|
|
256
|
+
...patch,
|
|
257
|
+
key,
|
|
258
|
+
quantity: normalizeQuantity(patch.quantity ?? lines[index].quantity)
|
|
259
|
+
};
|
|
260
|
+
return save(lines);
|
|
261
|
+
},
|
|
262
|
+
remove(key) {
|
|
263
|
+
return save(load().filter((line) => line.key !== key));
|
|
264
|
+
},
|
|
265
|
+
clear() {
|
|
266
|
+
try {
|
|
267
|
+
storage.removeItem(storageKey);
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
for (const listener of listeners) listener([]);
|
|
271
|
+
},
|
|
272
|
+
subscribe(listener) {
|
|
273
|
+
listeners.add(listener);
|
|
274
|
+
return () => void listeners.delete(listener);
|
|
275
|
+
},
|
|
276
|
+
toRequestItems() {
|
|
277
|
+
return load().map(({ key: _key, ...item }) => item);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/hooks.ts
|
|
164
283
|
function useQuote(token, options = {}) {
|
|
165
284
|
const client = useMemo(
|
|
166
285
|
() => new QuotesClient(options),
|
|
@@ -353,6 +472,31 @@ function useQuoteRequestForm(formToken, options = {}) {
|
|
|
353
472
|
);
|
|
354
473
|
return { config, loading, error, submitting, submitted, submit, formLoadedAt };
|
|
355
474
|
}
|
|
475
|
+
function useQuoteCart(options = {}) {
|
|
476
|
+
const cart = useMemo(
|
|
477
|
+
() => createQuoteCart(options),
|
|
478
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
479
|
+
[options.storageKey, options.maxLines]
|
|
480
|
+
);
|
|
481
|
+
const [lines, setLines] = useState([]);
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
setLines(cart.read());
|
|
484
|
+
return cart.subscribe(setLines);
|
|
485
|
+
}, [cart]);
|
|
486
|
+
return {
|
|
487
|
+
lines,
|
|
488
|
+
count: lines.reduce((sum, line) => sum + line.quantity, 0),
|
|
489
|
+
add: useCallback((item) => void cart.add(item), [cart]),
|
|
490
|
+
addMany: useCallback((items) => void cart.addMany(items), [cart]),
|
|
491
|
+
update: useCallback(
|
|
492
|
+
(key, patch) => void cart.update(key, patch),
|
|
493
|
+
[cart]
|
|
494
|
+
),
|
|
495
|
+
remove: useCallback((key) => void cart.remove(key), [cart]),
|
|
496
|
+
clear: useCallback(() => cart.clear(), [cart]),
|
|
497
|
+
cart
|
|
498
|
+
};
|
|
499
|
+
}
|
|
356
500
|
|
|
357
501
|
// src/QuoteView.tsx
|
|
358
502
|
import { useState as useState2 } from "react";
|
|
@@ -595,11 +739,13 @@ function QuoteRequestForm({
|
|
|
595
739
|
formToken,
|
|
596
740
|
options,
|
|
597
741
|
turnstileToken,
|
|
742
|
+
items,
|
|
598
743
|
onSubmitted,
|
|
599
744
|
loadingFallback,
|
|
600
745
|
className
|
|
601
746
|
}) {
|
|
602
747
|
const f = useQuoteRequestForm(formToken, options ?? {});
|
|
748
|
+
const lines = items ?? [];
|
|
603
749
|
const [values, setValues] = useState3({
|
|
604
750
|
email: "",
|
|
605
751
|
name: "",
|
|
@@ -629,7 +775,8 @@ function QuoteRequestForm({
|
|
|
629
775
|
e.preventDefault();
|
|
630
776
|
void f.submit({
|
|
631
777
|
email: values.email,
|
|
632
|
-
message: values.message,
|
|
778
|
+
message: values.message || null,
|
|
779
|
+
...lines.length > 0 ? { items: lines } : {},
|
|
633
780
|
name: values.name || null,
|
|
634
781
|
companyName: fields.companyName ? values.companyName || null : null,
|
|
635
782
|
phone: fields.phone ? values.phone || null : null,
|
|
@@ -663,9 +810,23 @@ function QuoteRequestForm({
|
|
|
663
810
|
"Your reference #",
|
|
664
811
|
/* @__PURE__ */ jsx2("input", { value: values.buyerReference, onChange: set("buyerReference") })
|
|
665
812
|
] }) : null,
|
|
813
|
+
lines.length > 0 ? /* @__PURE__ */ jsx2("ul", { "aria-label": "Items on this request", children: lines.map((line, index) => /* @__PURE__ */ jsxs2("li", { children: [
|
|
814
|
+
line.quantity ?? 1,
|
|
815
|
+
" \xD7 ",
|
|
816
|
+
line.name,
|
|
817
|
+
line.sku ? ` (${line.sku})` : "",
|
|
818
|
+
line.note ? ` \u2014 ${line.note}` : ""
|
|
819
|
+
] }, line.sku ?? line.referenceId ?? `${line.name}-${index}`)) }) : null,
|
|
666
820
|
/* @__PURE__ */ jsxs2("label", { children: [
|
|
667
|
-
"What do you need?",
|
|
668
|
-
/* @__PURE__ */ jsx2(
|
|
821
|
+
lines.length > 0 ? "Anything else we should know?" : "What do you need?",
|
|
822
|
+
/* @__PURE__ */ jsx2(
|
|
823
|
+
"textarea",
|
|
824
|
+
{
|
|
825
|
+
value: values.message,
|
|
826
|
+
onChange: set("message"),
|
|
827
|
+
required: lines.length === 0
|
|
828
|
+
}
|
|
829
|
+
)
|
|
669
830
|
] }),
|
|
670
831
|
/* @__PURE__ */ jsx2("div", { style: honeypotStyle, "aria-hidden": "true", children: /* @__PURE__ */ jsxs2("label", { children: [
|
|
671
832
|
"Website",
|
|
@@ -680,7 +841,14 @@ function QuoteRequestForm({
|
|
|
680
841
|
}
|
|
681
842
|
)
|
|
682
843
|
] }) }),
|
|
683
|
-
/* @__PURE__ */ jsx2(
|
|
844
|
+
/* @__PURE__ */ jsx2(
|
|
845
|
+
"button",
|
|
846
|
+
{
|
|
847
|
+
type: "submit",
|
|
848
|
+
disabled: f.submitting || !values.email || !values.message && lines.length === 0,
|
|
849
|
+
children: f.submitting ? "Sending\u2026" : "Request a quote"
|
|
850
|
+
}
|
|
851
|
+
)
|
|
684
852
|
]
|
|
685
853
|
}
|
|
686
854
|
);
|
|
@@ -691,10 +859,12 @@ export {
|
|
|
691
859
|
QuotesClient,
|
|
692
860
|
ThrottleQuotesError,
|
|
693
861
|
computeSelectionTotal,
|
|
862
|
+
createQuoteCart,
|
|
694
863
|
createQuotesClient,
|
|
695
864
|
formatMoney,
|
|
696
865
|
scaleLineCharges,
|
|
697
866
|
useQuote,
|
|
867
|
+
useQuoteCart,
|
|
698
868
|
useQuoteRequestForm
|
|
699
869
|
};
|
|
700
870
|
//# sourceMappingURL=index.js.map
|