@bluecircuit/offer-calculator 2.0.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.mjs ADDED
@@ -0,0 +1,1062 @@
1
+ import { useState, useMemo, useCallback } from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/lib/calculator/hooks.ts
5
+
6
+ // src/lib/calculator/pricing.ts
7
+ function calculateOffer(basePrice, conditionMultiplier, selectedDefects, minimumOffer = 1) {
8
+ const parsedBasePrice = parseFloat(String(basePrice));
9
+ const parsedConditionMultiplier = parseFloat(String(conditionMultiplier));
10
+ const parsedMinimumOffer = parseFloat(String(minimumOffer));
11
+ if (isNaN(parsedBasePrice) || parsedBasePrice <= 0 || !isFinite(parsedBasePrice)) {
12
+ console.error(
13
+ "[calculateOffer] Invalid base price:",
14
+ basePrice,
15
+ "| Returning minimum offer"
16
+ );
17
+ return createMinimumOfferResult(
18
+ basePrice,
19
+ parsedConditionMultiplier || 1,
20
+ selectedDefects,
21
+ parsedMinimumOffer
22
+ );
23
+ }
24
+ const validConditionMultiplier = isNaN(parsedConditionMultiplier) || !isFinite(parsedConditionMultiplier) || parsedConditionMultiplier <= 0 ? 1 : parsedConditionMultiplier;
25
+ const validMinimumOffer = isNaN(parsedMinimumOffer) || !isFinite(parsedMinimumOffer) || parsedMinimumOffer <= 0 ? 1 : parsedMinimumOffer;
26
+ let price = parsedBasePrice * validConditionMultiplier;
27
+ const priceAfterCondition = price;
28
+ const validDefects = selectedDefects.filter((d) => {
29
+ const percent = parseFloat(String(d.percent));
30
+ if (isNaN(percent) || !isFinite(percent) || percent < 0 || percent > 100) {
31
+ console.warn("[calculateOffer] Invalid defect percent:", d);
32
+ return false;
33
+ }
34
+ return true;
35
+ });
36
+ const sortedDefects = [...validDefects].sort(
37
+ (a, b) => b.percent - a.percent
38
+ );
39
+ for (const defect of sortedDefects) {
40
+ const discountMultiplier = 1 - defect.percent / 100;
41
+ price = price * discountMultiplier;
42
+ }
43
+ const priceBeforeClamp = Math.round(price * 100) / 100;
44
+ const finalPrice = Math.max(validMinimumOffer, priceBeforeClamp);
45
+ const minimumEnforced = finalPrice === validMinimumOffer && priceBeforeClamp < validMinimumOffer;
46
+ if (minimumEnforced) {
47
+ console.warn(
48
+ `[calculateOffer] Minimum price enforced: $${priceBeforeClamp.toFixed(2)} \u2192 $${finalPrice.toFixed(2)}`
49
+ );
50
+ }
51
+ const totalDiscountPercent = sortedDefects.reduce(
52
+ (sum, defect) => sum + defect.percent,
53
+ 0
54
+ );
55
+ const breakdown = {
56
+ basePrice: parsedBasePrice,
57
+ conditionMultiplier: validConditionMultiplier,
58
+ priceAfterCondition,
59
+ appliedDefects: sortedDefects,
60
+ totalDiscountPercent,
61
+ priceBeforeClamp,
62
+ minimumEnforced
63
+ };
64
+ return {
65
+ finalPrice,
66
+ breakdown
67
+ };
68
+ }
69
+ function createMinimumOfferResult(basePrice, conditionMultiplier, _selectedDefects, minimumOffer) {
70
+ return {
71
+ finalPrice: minimumOffer,
72
+ breakdown: {
73
+ basePrice: parseFloat(String(basePrice)) || 0,
74
+ conditionMultiplier: conditionMultiplier || 1,
75
+ priceAfterCondition: 0,
76
+ appliedDefects: [],
77
+ totalDiscountPercent: 0,
78
+ priceBeforeClamp: 0,
79
+ minimumEnforced: true
80
+ }
81
+ };
82
+ }
83
+ function formatCurrency(price, currency = "USD", locale = "en-US") {
84
+ try {
85
+ return new Intl.NumberFormat(locale, {
86
+ style: "currency",
87
+ currency,
88
+ minimumFractionDigits: 2,
89
+ maximumFractionDigits: 2
90
+ }).format(price);
91
+ } catch (error) {
92
+ console.error("[formatCurrency] Error formatting:", error);
93
+ return `$${price.toFixed(2)}`;
94
+ }
95
+ }
96
+ function calculateEffectiveDiscount(selectedDefects) {
97
+ let multiplier = 1;
98
+ for (const defect of selectedDefects) {
99
+ const discountMultiplier = 1 - defect.percent / 100;
100
+ multiplier *= discountMultiplier;
101
+ }
102
+ const effectiveDiscount = (1 - multiplier) * 100;
103
+ return Math.round(effectiveDiscount * 100) / 100;
104
+ }
105
+
106
+ // src/lib/calculator/hooks.ts
107
+ function useOfferCalculator(config) {
108
+ const [selectedDefectIds, setSelectedDefectIds] = useState(
109
+ /* @__PURE__ */ new Set()
110
+ );
111
+ const [selectedConditionId, setSelectedConditionId] = useState(() => {
112
+ const defaultCondition = config.conditions.find((c) => c.isDefault);
113
+ return defaultCondition?.id || config.conditions[0]?.id || "";
114
+ });
115
+ const [hasDefects, setHasDefects] = useState(false);
116
+ const selectedDefects = useMemo(() => {
117
+ return config.defects.filter((defect) => selectedDefectIds.has(defect.id));
118
+ }, [config.defects, selectedDefectIds]);
119
+ const currentCondition = useMemo(() => {
120
+ const found = config.conditions.find((c) => c.id === selectedConditionId);
121
+ if (found) return found;
122
+ const firstCondition = config.conditions[0];
123
+ if (!firstCondition) {
124
+ return {
125
+ id: "default",
126
+ label: "Default",
127
+ percent: 100,
128
+ multiplier: 1,
129
+ description: "Default condition",
130
+ isDefault: true
131
+ };
132
+ }
133
+ return firstCondition;
134
+ }, [config.conditions, selectedConditionId]);
135
+ const calculationResult = useMemo(() => {
136
+ return calculateOffer(
137
+ config.basePrice,
138
+ currentCondition.multiplier,
139
+ selectedDefects,
140
+ config.minimumOffer
141
+ );
142
+ }, [
143
+ config.basePrice,
144
+ config.minimumOffer,
145
+ currentCondition.multiplier,
146
+ selectedDefects
147
+ ]);
148
+ const toggleDefect = useCallback((defectId) => {
149
+ setSelectedDefectIds((prev) => {
150
+ const next = new Set(prev);
151
+ if (next.has(defectId)) {
152
+ next.delete(defectId);
153
+ } else {
154
+ next.add(defectId);
155
+ }
156
+ return next;
157
+ });
158
+ }, []);
159
+ const selectCondition = useCallback((conditionId) => {
160
+ setSelectedConditionId(conditionId);
161
+ setSelectedDefectIds(/* @__PURE__ */ new Set());
162
+ setHasDefects(false);
163
+ }, []);
164
+ const toggleHasDefects = useCallback(() => {
165
+ setHasDefects((prev) => {
166
+ const newValue = !prev;
167
+ if (!newValue) {
168
+ setSelectedDefectIds(/* @__PURE__ */ new Set());
169
+ }
170
+ return newValue;
171
+ });
172
+ }, []);
173
+ const clearDefects = useCallback(() => {
174
+ setSelectedDefectIds(/* @__PURE__ */ new Set());
175
+ }, []);
176
+ return {
177
+ // State
178
+ selectedDefectIds,
179
+ selectedConditionId,
180
+ hasDefects,
181
+ // Handlers
182
+ toggleDefect,
183
+ selectCondition,
184
+ toggleHasDefects,
185
+ clearDefects,
186
+ // Derived values
187
+ calculationResult,
188
+ selectedDefects,
189
+ currentCondition
190
+ };
191
+ }
192
+ function useDefectCheckbox(defectId, isSelected, onToggle) {
193
+ const handleChange = useCallback(() => {
194
+ onToggle(defectId);
195
+ }, [defectId, onToggle]);
196
+ return {
197
+ checked: isSelected,
198
+ handleChange
199
+ };
200
+ }
201
+ function ConditionTabs({
202
+ conditions,
203
+ selectedConditionId,
204
+ onSelectCondition,
205
+ className = ""
206
+ }) {
207
+ const handleKeyDown = useCallback(
208
+ (event, currentIndex) => {
209
+ let newIndex = currentIndex;
210
+ switch (event.key) {
211
+ case "ArrowRight":
212
+ case "ArrowDown":
213
+ event.preventDefault();
214
+ newIndex = (currentIndex + 1) % conditions.length;
215
+ break;
216
+ case "ArrowLeft":
217
+ case "ArrowUp":
218
+ event.preventDefault();
219
+ newIndex = currentIndex === 0 ? conditions.length - 1 : currentIndex - 1;
220
+ break;
221
+ case "Home":
222
+ event.preventDefault();
223
+ newIndex = 0;
224
+ break;
225
+ case "End":
226
+ event.preventDefault();
227
+ newIndex = conditions.length - 1;
228
+ break;
229
+ default:
230
+ return;
231
+ }
232
+ const nextCondition = conditions[newIndex];
233
+ if (nextCondition) {
234
+ onSelectCondition(nextCondition.id);
235
+ }
236
+ },
237
+ [conditions, onSelectCondition]
238
+ );
239
+ return /* @__PURE__ */ jsxs("div", { className: `condition-tabs ${className}`, children: [
240
+ /* @__PURE__ */ jsx(
241
+ "div",
242
+ {
243
+ role: "tablist",
244
+ "aria-label": "Device condition",
245
+ className: "condition-tabs__list",
246
+ children: conditions.map((condition, index) => {
247
+ const isSelected = condition.id === selectedConditionId;
248
+ return /* @__PURE__ */ jsxs(
249
+ "button",
250
+ {
251
+ role: "tab",
252
+ "aria-selected": isSelected,
253
+ "aria-controls": `condition-panel-${condition.id}`,
254
+ id: `condition-tab-${condition.id}`,
255
+ tabIndex: isSelected ? 0 : -1,
256
+ onClick: () => onSelectCondition(condition.id),
257
+ onKeyDown: (e) => handleKeyDown(e, index),
258
+ className: `condition-tabs__tab ${isSelected ? "condition-tabs__tab--active" : ""}`,
259
+ title: condition.description,
260
+ children: [
261
+ /* @__PURE__ */ jsx("span", { className: "condition-tabs__tab-label", children: condition.label }),
262
+ /* @__PURE__ */ jsxs("span", { className: "condition-tabs__tab-multiplier", children: [
263
+ condition.percent,
264
+ "%"
265
+ ] })
266
+ ]
267
+ },
268
+ condition.id
269
+ );
270
+ })
271
+ }
272
+ ),
273
+ conditions.map((condition) => {
274
+ const isSelected = condition.id === selectedConditionId;
275
+ return /* @__PURE__ */ jsx(
276
+ "div",
277
+ {
278
+ role: "tabpanel",
279
+ id: `condition-panel-${condition.id}`,
280
+ "aria-labelledby": `condition-tab-${condition.id}`,
281
+ hidden: !isSelected,
282
+ className: "condition-tabs__panel",
283
+ children: /* @__PURE__ */ jsx("p", { className: "condition-tabs__description", children: condition.description })
284
+ },
285
+ condition.id
286
+ );
287
+ })
288
+ ] });
289
+ }
290
+ function CompactConditionSelector({
291
+ conditions,
292
+ selectedConditionId,
293
+ onSelectCondition,
294
+ className = ""
295
+ }) {
296
+ const handleChange = (event) => {
297
+ onSelectCondition(event.target.value);
298
+ };
299
+ const selectedCondition = conditions.find(
300
+ (c) => c.id === selectedConditionId
301
+ );
302
+ return /* @__PURE__ */ jsxs("div", { className: `condition-selector ${className}`, children: [
303
+ /* @__PURE__ */ jsx("label", { htmlFor: "condition-select", className: "condition-selector__label", children: "Device Condition:" }),
304
+ /* @__PURE__ */ jsx(
305
+ "select",
306
+ {
307
+ id: "condition-select",
308
+ value: selectedConditionId,
309
+ onChange: handleChange,
310
+ className: "condition-selector__select",
311
+ "aria-describedby": "condition-description",
312
+ children: conditions.map((condition) => /* @__PURE__ */ jsxs("option", { value: condition.id, children: [
313
+ condition.label,
314
+ " (",
315
+ condition.percent,
316
+ "%)"
317
+ ] }, condition.id))
318
+ }
319
+ ),
320
+ selectedCondition && /* @__PURE__ */ jsx(
321
+ "p",
322
+ {
323
+ id: "condition-description",
324
+ className: "condition-selector__description",
325
+ children: selectedCondition.description
326
+ }
327
+ )
328
+ ] });
329
+ }
330
+ function DefectCheckbox({
331
+ defect,
332
+ isSelected,
333
+ onToggle,
334
+ disabled = false,
335
+ className = ""
336
+ }) {
337
+ const { checked, handleChange } = useDefectCheckbox(
338
+ defect.id,
339
+ isSelected,
340
+ onToggle
341
+ );
342
+ const [tooltipVisible, setTooltipVisible] = useState(false);
343
+ const checkboxId = `defect-${defect.id}`;
344
+ const tooltipId = defect.description ? `defect-tooltip-${defect.id}` : void 0;
345
+ return /* @__PURE__ */ jsxs("div", { className: `defect-checkbox ${className}`, children: [
346
+ /* @__PURE__ */ jsxs(
347
+ "label",
348
+ {
349
+ htmlFor: checkboxId,
350
+ className: "defect-checkbox__label",
351
+ onMouseEnter: () => setTooltipVisible(true),
352
+ onMouseLeave: () => setTooltipVisible(false),
353
+ onFocus: () => setTooltipVisible(true),
354
+ onBlur: () => setTooltipVisible(false),
355
+ children: [
356
+ /* @__PURE__ */ jsx(
357
+ "input",
358
+ {
359
+ type: "checkbox",
360
+ id: checkboxId,
361
+ checked,
362
+ onChange: handleChange,
363
+ disabled,
364
+ "aria-describedby": tooltipId,
365
+ className: "defect-checkbox__input"
366
+ }
367
+ ),
368
+ /* @__PURE__ */ jsxs("span", { className: "defect-checkbox__text", children: [
369
+ defect.label,
370
+ /* @__PURE__ */ jsxs("span", { className: "defect-checkbox__percent", children: [
371
+ " ",
372
+ "(",
373
+ defect.percent,
374
+ "% off)"
375
+ ] })
376
+ ] }),
377
+ defect.description && /* @__PURE__ */ jsx("span", { className: "defect-checkbox__info-icon", "aria-hidden": "true", children: "\u2139\uFE0F" })
378
+ ]
379
+ }
380
+ ),
381
+ defect.description && tooltipVisible && /* @__PURE__ */ jsx(
382
+ "div",
383
+ {
384
+ id: tooltipId,
385
+ role: "tooltip",
386
+ className: "defect-checkbox__tooltip",
387
+ "aria-hidden": !tooltipVisible,
388
+ children: defect.description
389
+ }
390
+ )
391
+ ] });
392
+ }
393
+ function DefectCheckboxList({
394
+ defects,
395
+ selectedDefectIds,
396
+ onToggle,
397
+ disabled = false,
398
+ groupByCategory = false,
399
+ className = ""
400
+ }) {
401
+ if (!groupByCategory) {
402
+ return /* @__PURE__ */ jsx("div", { className: `defect-checkbox-list ${className}`, children: defects.map((defect) => /* @__PURE__ */ jsx(
403
+ DefectCheckbox,
404
+ {
405
+ defect,
406
+ isSelected: selectedDefectIds.has(defect.id),
407
+ onToggle,
408
+ disabled
409
+ },
410
+ defect.id
411
+ )) });
412
+ }
413
+ const grouped = defects.reduce(
414
+ (acc, defect) => {
415
+ const category = defect.category || "other";
416
+ if (!acc[category]) {
417
+ acc[category] = [];
418
+ }
419
+ acc[category].push(defect);
420
+ return acc;
421
+ },
422
+ {}
423
+ );
424
+ return /* @__PURE__ */ jsx("div", { className: `defect-checkbox-list defect-checkbox-list--grouped ${className}`, children: Object.entries(grouped).map(([category, categoryDefects]) => /* @__PURE__ */ jsxs("div", { className: "defect-checkbox-list__group", children: [
425
+ /* @__PURE__ */ jsx("h4", { className: "defect-checkbox-list__group-title", children: category.charAt(0).toUpperCase() + category.slice(1) }),
426
+ categoryDefects.map((defect) => /* @__PURE__ */ jsx(
427
+ DefectCheckbox,
428
+ {
429
+ defect,
430
+ isSelected: selectedDefectIds.has(defect.id),
431
+ onToggle,
432
+ disabled
433
+ },
434
+ defect.id
435
+ ))
436
+ ] }, category)) });
437
+ }
438
+ function PriceDisplay({
439
+ result,
440
+ currency = "USD",
441
+ locale = "en-US",
442
+ showBreakdown = false,
443
+ className = ""
444
+ }) {
445
+ const { finalPrice, breakdown } = result;
446
+ const formattedPrice = formatCurrency(finalPrice, currency, locale);
447
+ return /* @__PURE__ */ jsxs("div", { className: `price-display ${className}`, children: [
448
+ /* @__PURE__ */ jsxs("div", { className: "price-display__main", children: [
449
+ /* @__PURE__ */ jsx("span", { className: "price-display__label", children: "Your Offer:" }),
450
+ /* @__PURE__ */ jsx(
451
+ "span",
452
+ {
453
+ className: "price-display__value",
454
+ "aria-live": "polite",
455
+ "aria-atomic": "true",
456
+ children: formattedPrice
457
+ }
458
+ )
459
+ ] }),
460
+ breakdown.minimumEnforced && /* @__PURE__ */ jsxs(
461
+ "div",
462
+ {
463
+ className: "price-display__warning",
464
+ role: "alert",
465
+ "aria-live": "assertive",
466
+ children: [
467
+ "\u26A0\uFE0F Minimum offer of ",
468
+ formatCurrency(finalPrice, currency, locale),
469
+ " ",
470
+ "enforced"
471
+ ]
472
+ }
473
+ ),
474
+ showBreakdown && /* @__PURE__ */ jsxs("div", { className: "price-display__breakdown", children: [
475
+ /* @__PURE__ */ jsx("h4", { children: "Calculation Breakdown" }),
476
+ /* @__PURE__ */ jsxs("dl", { className: "price-display__breakdown-list", children: [
477
+ /* @__PURE__ */ jsx("dt", { children: "Base Price:" }),
478
+ /* @__PURE__ */ jsx("dd", { children: formatCurrency(breakdown.basePrice, currency, locale) }),
479
+ /* @__PURE__ */ jsx("dt", { children: "Condition Multiplier:" }),
480
+ /* @__PURE__ */ jsxs("dd", { children: [
481
+ breakdown.conditionMultiplier.toFixed(2),
482
+ "x"
483
+ ] }),
484
+ /* @__PURE__ */ jsx("dt", { children: "Price After Condition:" }),
485
+ /* @__PURE__ */ jsx("dd", { children: formatCurrency(breakdown.priceAfterCondition, currency, locale) }),
486
+ breakdown.appliedDefects.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
487
+ /* @__PURE__ */ jsx("dt", { children: "Applied Defects:" }),
488
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("ul", { children: breakdown.appliedDefects.map((defect) => /* @__PURE__ */ jsxs("li", { children: [
489
+ defect.label,
490
+ " (",
491
+ defect.percent,
492
+ "%)"
493
+ ] }, defect.id)) }) }),
494
+ /* @__PURE__ */ jsx("dt", { children: "Total Discount (additive):" }),
495
+ /* @__PURE__ */ jsxs("dd", { children: [
496
+ breakdown.totalDiscountPercent.toFixed(2),
497
+ "%"
498
+ ] })
499
+ ] }),
500
+ /* @__PURE__ */ jsx("dt", { children: "Price Before Minimum:" }),
501
+ /* @__PURE__ */ jsx("dd", { children: formatCurrency(breakdown.priceBeforeClamp, currency, locale) }),
502
+ /* @__PURE__ */ jsx("dt", { children: "Final Offer:" }),
503
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("strong", { children: formattedPrice }) })
504
+ ] })
505
+ ] })
506
+ ] });
507
+ }
508
+ function CompactPriceDisplay({
509
+ result,
510
+ currency = "USD",
511
+ locale = "en-US",
512
+ className = ""
513
+ }) {
514
+ const formattedPrice = formatCurrency(result.finalPrice, currency, locale);
515
+ return /* @__PURE__ */ jsx(
516
+ "span",
517
+ {
518
+ className: `price-display--compact ${className}`,
519
+ "aria-live": "polite",
520
+ "aria-atomic": "true",
521
+ children: formattedPrice
522
+ }
523
+ );
524
+ }
525
+
526
+ // src/components/calculator/calculator.module.css
527
+ var calculator_default = {};
528
+ function OfferCalculator({
529
+ config,
530
+ showBreakdown = false,
531
+ className = "",
532
+ formFieldPrefix = ""
533
+ }) {
534
+ const {
535
+ selectedDefectIds,
536
+ selectedConditionId,
537
+ hasDefects,
538
+ toggleDefect,
539
+ selectCondition,
540
+ toggleHasDefects,
541
+ calculationResult,
542
+ currentCondition
543
+ } = useOfferCalculator(config);
544
+ return /* @__PURE__ */ jsxs("div", { className: `${calculator_default.calculator} ${className}`, children: [
545
+ /* @__PURE__ */ jsxs("div", { className: calculator_default.calculator__header, children: [
546
+ /* @__PURE__ */ jsxs("h2", { className: calculator_default.calculator__title, children: [
547
+ "Get an Offer for Your ",
548
+ config.device.model
549
+ ] }),
550
+ /* @__PURE__ */ jsx("p", { className: calculator_default.calculator__subtitle, children: "Select your device condition and any defects to see your offer." })
551
+ ] }),
552
+ /* @__PURE__ */ jsxs("section", { className: calculator_default.calculator__section, children: [
553
+ /* @__PURE__ */ jsx("h3", { className: calculator_default.calculator__sectionTitle, children: "1. Select Device Condition" }),
554
+ /* @__PURE__ */ jsx(
555
+ ConditionTabs,
556
+ {
557
+ conditions: config.conditions,
558
+ selectedConditionId,
559
+ onSelectCondition: selectCondition
560
+ }
561
+ )
562
+ ] }),
563
+ /* @__PURE__ */ jsxs("section", { className: calculator_default.calculator__section, children: [
564
+ /* @__PURE__ */ jsx("h3", { className: calculator_default.calculator__sectionTitle, children: "2. Does Your Device Have Any Defects?" }),
565
+ /* @__PURE__ */ jsxs("div", { className: calculator_default.calculator__defectsToggle, children: [
566
+ /* @__PURE__ */ jsxs("label", { className: calculator_default.calculator__radioLabel, children: [
567
+ /* @__PURE__ */ jsx(
568
+ "input",
569
+ {
570
+ type: "radio",
571
+ name: "has_defects",
572
+ value: "no",
573
+ checked: !hasDefects,
574
+ onChange: () => {
575
+ if (hasDefects) toggleHasDefects();
576
+ },
577
+ className: calculator_default.calculator__radioInput
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsx("span", { children: "No defects" })
581
+ ] }),
582
+ /* @__PURE__ */ jsxs("label", { className: calculator_default.calculator__radioLabel, children: [
583
+ /* @__PURE__ */ jsx(
584
+ "input",
585
+ {
586
+ type: "radio",
587
+ name: "has_defects",
588
+ value: "yes",
589
+ checked: hasDefects,
590
+ onChange: () => {
591
+ if (!hasDefects) toggleHasDefects();
592
+ },
593
+ className: calculator_default.calculator__radioInput
594
+ }
595
+ ),
596
+ /* @__PURE__ */ jsx("span", { children: "Yes, has defects" })
597
+ ] })
598
+ ] })
599
+ ] }),
600
+ hasDefects && config.defects.length > 0 && /* @__PURE__ */ jsxs(
601
+ "section",
602
+ {
603
+ className: calculator_default.calculator__section,
604
+ "data-testid": "defects-section",
605
+ children: [
606
+ /* @__PURE__ */ jsx("h3", { className: calculator_default.calculator__sectionTitle, children: "3. Select All Defects" }),
607
+ /* @__PURE__ */ jsx("p", { className: calculator_default.calculator__hint, children: "Select all that apply. Order doesn't matter." }),
608
+ /* @__PURE__ */ jsx(
609
+ DefectCheckboxList,
610
+ {
611
+ defects: config.defects,
612
+ selectedDefectIds,
613
+ onToggle: toggleDefect,
614
+ groupByCategory: false
615
+ }
616
+ )
617
+ ]
618
+ }
619
+ ),
620
+ /* @__PURE__ */ jsx("section", { className: calculator_default.calculator__section, children: /* @__PURE__ */ jsx(
621
+ PriceDisplay,
622
+ {
623
+ result: calculationResult,
624
+ currency: config.currency,
625
+ showBreakdown
626
+ }
627
+ ) }),
628
+ /* @__PURE__ */ jsx(
629
+ HiddenFormInputs,
630
+ {
631
+ finalPrice: calculationResult.finalPrice,
632
+ conditionId: currentCondition.id,
633
+ selectedDefectIds: Array.from(selectedDefectIds),
634
+ deviceId: String(config.device.id),
635
+ fieldPrefix: formFieldPrefix
636
+ }
637
+ )
638
+ ] });
639
+ }
640
+ function HiddenFormInputs({
641
+ finalPrice,
642
+ conditionId,
643
+ selectedDefectIds,
644
+ deviceId,
645
+ fieldPrefix = ""
646
+ }) {
647
+ const prefix = fieldPrefix ? `${fieldPrefix}_` : "";
648
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
649
+ /* @__PURE__ */ jsx(
650
+ "input",
651
+ {
652
+ type: "hidden",
653
+ name: `${prefix}device_price`,
654
+ value: finalPrice.toFixed(2)
655
+ }
656
+ ),
657
+ /* @__PURE__ */ jsx(
658
+ "input",
659
+ {
660
+ type: "hidden",
661
+ name: `${prefix}gadget_cosmetic_condition`,
662
+ value: conditionId
663
+ }
664
+ ),
665
+ /* @__PURE__ */ jsx(
666
+ "input",
667
+ {
668
+ type: "hidden",
669
+ name: `${prefix}selected_defects`,
670
+ value: JSON.stringify(selectedDefectIds)
671
+ }
672
+ ),
673
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: `${prefix}device_id`, value: deviceId })
674
+ ] });
675
+ }
676
+ function CalculatorErrorBoundary({
677
+ error,
678
+ reset
679
+ }) {
680
+ return /* @__PURE__ */ jsxs("div", { className: calculator_default.calculator__error, children: [
681
+ /* @__PURE__ */ jsx("h2", { children: "Calculator Error" }),
682
+ /* @__PURE__ */ jsx("p", { children: "We're sorry, but the calculator encountered an error. Please try again or contact support." }),
683
+ reset && /* @__PURE__ */ jsx("button", { onClick: reset, className: calculator_default.calculator__errorButton, children: "Try Again" }),
684
+ process.env.NODE_ENV === "development" && /* @__PURE__ */ jsxs("details", { className: calculator_default.calculator__errorDetails, children: [
685
+ /* @__PURE__ */ jsx("summary", { children: "Error Details (dev only)" }),
686
+ /* @__PURE__ */ jsx("pre", { children: error.message }),
687
+ /* @__PURE__ */ jsx("pre", { children: error.stack })
688
+ ] })
689
+ ] });
690
+ }
691
+
692
+ // src/lib/calculator/validation.ts
693
+ function normalizeConfig(raw) {
694
+ const basePrice = parseFloat(raw?.basePrice);
695
+ const validBasePrice = !isNaN(basePrice) && isFinite(basePrice) && basePrice > 0 ? basePrice : 1;
696
+ const currency = typeof raw?.currency === "string" && raw.currency.length > 0 ? raw.currency : "USD";
697
+ const defects = normalizeDefects(raw?.defects);
698
+ const conditions = normalizeConditions(raw?.conditions);
699
+ const device = normalizeDeviceInfo(raw?.device);
700
+ const minimumOffer = parseFloat(raw?.minimumOffer);
701
+ const validMinimumOffer = !isNaN(minimumOffer) && isFinite(minimumOffer) && minimumOffer > 0 ? minimumOffer : 1;
702
+ return {
703
+ basePrice: validBasePrice,
704
+ currency,
705
+ defects,
706
+ conditions,
707
+ device,
708
+ minimumOffer: validMinimumOffer
709
+ };
710
+ }
711
+ function normalizeDefects(raw) {
712
+ if (!Array.isArray(raw)) {
713
+ console.warn("[normalizeConfig] Defects is not an array:", raw);
714
+ return [];
715
+ }
716
+ return raw.filter((item) => {
717
+ if (!item?.id || typeof item.label !== "string") {
718
+ console.warn("[normalizeConfig] Invalid defect (missing id/label):", item);
719
+ return false;
720
+ }
721
+ return true;
722
+ }).map((item) => {
723
+ const percent = parseFloat(item.percent);
724
+ const validPercent = !isNaN(percent) && isFinite(percent) ? Math.max(0, Math.min(100, percent)) : 0;
725
+ const defect = {
726
+ id: String(item.id),
727
+ label: String(item.label),
728
+ percent: validPercent
729
+ };
730
+ if (item.description) {
731
+ defect.description = String(item.description);
732
+ }
733
+ if (item.category) {
734
+ defect.category = String(item.category);
735
+ }
736
+ return defect;
737
+ });
738
+ }
739
+ function normalizeConditions(raw) {
740
+ if (!Array.isArray(raw)) {
741
+ console.warn("[normalizeConfig] Conditions is not an array:", raw);
742
+ return createDefaultConditions();
743
+ }
744
+ const normalized = raw.filter((item) => {
745
+ if (!item?.id || !item?.label) {
746
+ console.warn("[normalizeConfig] Invalid condition (missing id/label):", item);
747
+ return false;
748
+ }
749
+ return true;
750
+ }).map((item) => {
751
+ const percent = parseFloat(item.percent);
752
+ const validPercent = !isNaN(percent) && isFinite(percent) && percent > 0 ? percent : 100;
753
+ const multiplier = validPercent / 100;
754
+ const condition = {
755
+ id: String(item.id),
756
+ label: String(item.label),
757
+ percent: validPercent,
758
+ multiplier,
759
+ description: item.description ? String(item.description) : ""
760
+ };
761
+ if (item.isDefault) {
762
+ condition.isDefault = true;
763
+ }
764
+ return condition;
765
+ });
766
+ if (normalized.length === 0) {
767
+ return createDefaultConditions();
768
+ }
769
+ return normalized;
770
+ }
771
+ function createDefaultConditions() {
772
+ return [
773
+ {
774
+ id: "flawless",
775
+ label: "Flawless",
776
+ percent: 100,
777
+ multiplier: 1,
778
+ description: "Like New, no visible signs of previous usage.",
779
+ isDefault: true
780
+ }
781
+ ];
782
+ }
783
+ function normalizeDeviceInfo(raw) {
784
+ return {
785
+ id: raw?.id !== void 0 ? String(raw.id) : "unknown",
786
+ model: raw?.model ? String(raw.model) : "Unknown Device",
787
+ type: raw?.type ? String(raw.type) : "device"
788
+ };
789
+ }
790
+ function validateConfig(config) {
791
+ const errors = [];
792
+ if (config.basePrice <= 0) {
793
+ errors.push({
794
+ field: "basePrice",
795
+ message: "Base price must be positive",
796
+ severity: "error"
797
+ });
798
+ }
799
+ if (config.minimumOffer <= 0) {
800
+ errors.push({
801
+ field: "minimumOffer",
802
+ message: "Minimum offer must be positive",
803
+ severity: "error"
804
+ });
805
+ }
806
+ if (config.defects.length === 0) {
807
+ errors.push({
808
+ field: "defects",
809
+ message: "No defects configured",
810
+ severity: "warning"
811
+ });
812
+ }
813
+ if (config.conditions.length === 0) {
814
+ errors.push({
815
+ field: "conditions",
816
+ message: "No conditions configured",
817
+ severity: "error"
818
+ });
819
+ }
820
+ const defectIds = /* @__PURE__ */ new Set();
821
+ for (const defect of config.defects) {
822
+ if (defectIds.has(defect.id)) {
823
+ errors.push({
824
+ field: "defects",
825
+ message: `Duplicate defect ID: ${defect.id}`,
826
+ severity: "error"
827
+ });
828
+ }
829
+ defectIds.add(defect.id);
830
+ }
831
+ const conditionIds = /* @__PURE__ */ new Set();
832
+ for (const condition of config.conditions) {
833
+ if (conditionIds.has(condition.id)) {
834
+ errors.push({
835
+ field: "conditions",
836
+ message: `Duplicate condition ID: ${condition.id}`,
837
+ severity: "error"
838
+ });
839
+ }
840
+ conditionIds.add(condition.id);
841
+ }
842
+ const hasDefault = config.conditions.some((c) => c.isDefault);
843
+ if (!hasDefault && config.conditions.length > 0) {
844
+ errors.push({
845
+ field: "conditions",
846
+ message: "No condition marked as default",
847
+ severity: "warning"
848
+ });
849
+ }
850
+ return errors;
851
+ }
852
+ function hasErrors(errors) {
853
+ return errors.some((e) => e.severity === "error");
854
+ }
855
+ function formatValidationErrors(errors) {
856
+ if (errors.length === 0) {
857
+ return "";
858
+ }
859
+ return errors.map((error) => {
860
+ const prefix = error.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
861
+ return `${prefix} ${error.field}: ${error.message}`;
862
+ }).join("\n");
863
+ }
864
+
865
+ // src/lib/cms/fetchCalculatorConfig.ts
866
+ async function fetchCalculatorConfig(deviceId) {
867
+ const apiUrl = process.env.CALCULATOR_API_URL || "/api/calculator/config";
868
+ const url = `${apiUrl}?deviceId=${encodeURIComponent(deviceId)}`;
869
+ try {
870
+ const response = await fetch(url, {
871
+ next: {
872
+ revalidate: 3600,
873
+ // Cache for 1 hour
874
+ tags: [`calculator-config-${deviceId}`]
875
+ // For on-demand revalidation
876
+ }
877
+ });
878
+ if (!response.ok) {
879
+ throw new Error(
880
+ `Failed to fetch calculator config: ${response.status} ${response.statusText}`
881
+ );
882
+ }
883
+ const rawData = await response.json();
884
+ const config = normalizeConfig(rawData);
885
+ const validationErrors = validateConfig(config);
886
+ const warnings = validationErrors.filter((e) => e.severity === "warning");
887
+ if (warnings.length > 0) {
888
+ console.warn(
889
+ "[fetchCalculatorConfig] Validation warnings:",
890
+ formatValidationErrors(warnings)
891
+ );
892
+ }
893
+ if (hasErrors(validationErrors)) {
894
+ const errorMessage = formatValidationErrors(
895
+ validationErrors.filter((e) => e.severity === "error")
896
+ );
897
+ throw new Error(
898
+ `Calculator config validation failed:
899
+ ${errorMessage}`
900
+ );
901
+ }
902
+ return config;
903
+ } catch (error) {
904
+ console.error("[fetchCalculatorConfig] Error fetching config:", error);
905
+ throw new Error(
906
+ `Failed to load calculator configuration for device ${deviceId}: ${error instanceof Error ? error.message : "Unknown error"}`
907
+ );
908
+ }
909
+ }
910
+ function getMockCalculatorConfig(deviceId) {
911
+ return {
912
+ basePrice: 230,
913
+ currency: "USD",
914
+ defects: [
915
+ {
916
+ id: "check1",
917
+ label: "Motherboard issues/Doesn't power on",
918
+ percent: 60,
919
+ description: "Select if device has defective MB, BIOS locked, or won't boot. Major discount.",
920
+ category: "critical"
921
+ },
922
+ {
923
+ id: "check2",
924
+ label: "Cracked/broken screen",
925
+ percent: 45,
926
+ description: "Select if screen has cracks, dead pixels, or display issues.",
927
+ category: "critical"
928
+ },
929
+ {
930
+ id: "check3",
931
+ label: "Hard drive missing/defective",
932
+ percent: 15,
933
+ description: "Select if HDD/SSD is missing or not working.",
934
+ category: "hardware"
935
+ },
936
+ {
937
+ id: "check5",
938
+ label: "Charger/adapter missing",
939
+ percent: 15,
940
+ description: "Select if power adapter is not included.",
941
+ category: "accessories"
942
+ },
943
+ {
944
+ id: "check6",
945
+ label: "Battery issues",
946
+ percent: 15,
947
+ description: "Select if battery doesn't hold charge or is swollen.",
948
+ category: "hardware"
949
+ },
950
+ {
951
+ id: "check7",
952
+ label: "Keyboard/touchpad issues",
953
+ percent: 20,
954
+ description: "Select if keys are missing or input devices not working.",
955
+ category: "hardware"
956
+ },
957
+ {
958
+ id: "check11",
959
+ label: "CPU/processor issues",
960
+ percent: 30,
961
+ description: "Select if CPU is defective or underperforming.",
962
+ category: "hardware"
963
+ },
964
+ {
965
+ id: "check13",
966
+ label: "RAM issues",
967
+ percent: 15,
968
+ description: "Select if memory is defective or missing.",
969
+ category: "hardware"
970
+ },
971
+ {
972
+ id: "check16",
973
+ label: "Damaged housing/case",
974
+ percent: 30,
975
+ description: "Select if case has major dents, cracks, or damage.",
976
+ category: "cosmetic"
977
+ },
978
+ {
979
+ id: "check20",
980
+ label: "Graphics card issues",
981
+ percent: 45,
982
+ description: "Select if GPU is defective or has display artifacts.",
983
+ category: "hardware"
984
+ },
985
+ {
986
+ id: "check4",
987
+ label: "Optical drive missing/defective",
988
+ percent: 15,
989
+ description: "Select if CD/DVD drive is missing or not working.",
990
+ category: "hardware"
991
+ },
992
+ {
993
+ id: "check9",
994
+ label: "Overheating issues",
995
+ percent: 30,
996
+ description: "Select if device overheats or fan doesn't work.",
997
+ category: "hardware"
998
+ },
999
+ {
1000
+ id: "check12",
1001
+ label: "BIOS password locked",
1002
+ percent: 70,
1003
+ description: "Select if device has BIOS password and cannot be reset. Major discount.",
1004
+ category: "critical"
1005
+ }
1006
+ ],
1007
+ conditions: [
1008
+ {
1009
+ id: "flawless",
1010
+ label: "Flawless",
1011
+ percent: 100,
1012
+ multiplier: 1,
1013
+ description: "Like New, no visible signs of previous usage.",
1014
+ isDefault: true
1015
+ },
1016
+ {
1017
+ id: "good",
1018
+ label: "Good",
1019
+ percent: 85,
1020
+ multiplier: 0.85,
1021
+ description: "Light signs of use, minor scratches, fully functional. No dents."
1022
+ },
1023
+ {
1024
+ id: "fair",
1025
+ label: "Fair",
1026
+ percent: 75,
1027
+ multiplier: 0.75,
1028
+ description: "Moderate wear, scratches, scuffs. May have small dents. Fully functional."
1029
+ },
1030
+ {
1031
+ id: "poor",
1032
+ label: "Poor",
1033
+ percent: 60,
1034
+ multiplier: 0.6,
1035
+ description: "Heavy wear, significant damage. Dents, deep scratches. May have issues."
1036
+ }
1037
+ ],
1038
+ device: {
1039
+ id: deviceId,
1040
+ model: "Dell G15 5510 Intel Core i5 10th Gen",
1041
+ type: "laptop"
1042
+ },
1043
+ minimumOffer: 1
1044
+ };
1045
+ }
1046
+ async function revalidateCalculatorConfig(deviceId) {
1047
+ try {
1048
+ const { revalidateTag } = await import('next/cache');
1049
+ revalidateTag(`calculator-config-${deviceId}`);
1050
+ console.log(`[revalidateCalculatorConfig] Cache cleared for ${deviceId}`);
1051
+ } catch (error) {
1052
+ console.error("[revalidateCalculatorConfig] Error:", error);
1053
+ throw error;
1054
+ }
1055
+ }
1056
+
1057
+ // src/index.ts
1058
+ var VERSION = "2.0.0";
1059
+
1060
+ export { CalculatorErrorBoundary, CompactConditionSelector, CompactPriceDisplay, ConditionTabs, DefectCheckbox, DefectCheckboxList, OfferCalculator, PriceDisplay, VERSION, calculateEffectiveDiscount, calculateOffer, fetchCalculatorConfig, formatCurrency, formatValidationErrors, getMockCalculatorConfig, hasErrors, normalizeConfig, revalidateCalculatorConfig, useDefectCheckbox, useOfferCalculator, validateConfig };
1061
+ //# sourceMappingURL=index.mjs.map
1062
+ //# sourceMappingURL=index.mjs.map