@forgewp/woocommerce 0.1.1

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.
@@ -0,0 +1,1848 @@
1
+ import React from "react";
2
+ import { WpPostContext } from "@forgewp/react";
3
+ import {
4
+ Heart,
5
+ Trash2,
6
+ Plus,
7
+ Minus,
8
+ Star,
9
+ Lock,
10
+ User,
11
+ Download,
12
+ SlidersHorizontal,
13
+ X,
14
+ ArrowRight
15
+ } from "lucide-react";
16
+ import {
17
+ WpCartContext,
18
+ CartItem,
19
+ CartState,
20
+ useWpCart,
21
+ useWpCheckout,
22
+ useWpCustomer,
23
+ useWpProductFilters,
24
+ useWpWishlist,
25
+ useWpProducts,
26
+ useWpProduct,
27
+ useWpProductCategories,
28
+ useWpProductTags,
29
+ fetchStoreApi,
30
+ mapStoreApiToCartState,
31
+ IS_DEV
32
+ } from "./hooks";
33
+
34
+ // ── 1. WpCartProvider ────────────────────────────────────────────────────────
35
+
36
+ export function WpCartProvider({ children }: { children: React.ReactNode }) {
37
+ const { products } = useWpProducts();
38
+ const [cart, setCart] = React.useState<CartState>({
39
+ items: [],
40
+ totals: { subtotal: "0.00", discount: "0.00", shipping: "0.00", tax: "0.00", total: "0.00" },
41
+ coupons: [],
42
+ shippingAddress: { country: "US", city: "", postcode: "" },
43
+ shippingMethod: "flat_rate"
44
+ });
45
+ const [isLoading, setIsLoading] = React.useState(false);
46
+
47
+ // Load from local storage or WooCommerce Store API
48
+ React.useEffect(() => {
49
+ if (typeof window !== "undefined") {
50
+ if (!IS_DEV) {
51
+ setIsLoading(true);
52
+ fetchStoreApi("cart")
53
+ .then((storeCart) => {
54
+ setCart(mapStoreApiToCartState(storeCart));
55
+ })
56
+ .catch((err) => {
57
+ console.error("Failed to fetch WooCommerce cart:", err);
58
+ })
59
+ .finally(() => {
60
+ setIsLoading(false);
61
+ });
62
+ } else {
63
+ const stored = localStorage.getItem("forgewp-cart");
64
+ if (stored) {
65
+ setCart(JSON.parse(stored));
66
+ }
67
+ }
68
+ }
69
+ }, []);
70
+
71
+ // Recalculate totals helper (used for dev mock fallback only)
72
+ const recalculateTotals = (items: CartItem[], coupons: string[], shippingMethod: string): CartState["totals"] => {
73
+ const subtotalNum = items.reduce((sum, item) => sum + parseFloat(item.line_subtotal), 0);
74
+
75
+ // Coupons calculation: "BRUTAL5" is 5% off, "STARK" is flat $10 off
76
+ let discountNum = 0;
77
+ coupons.forEach((code) => {
78
+ if (code.toUpperCase() === "BRUTAL5") {
79
+ discountNum += subtotalNum * 0.05;
80
+ } else if (code.toUpperCase() === "STARK") {
81
+ discountNum += 10.0;
82
+ }
83
+ });
84
+ if (discountNum > subtotalNum) discountNum = subtotalNum;
85
+
86
+ // Shipping cost
87
+ let shippingNum = 10.0;
88
+ if (subtotalNum > 100.0 || shippingMethod === "free_shipping") {
89
+ shippingNum = 0.0;
90
+ } else if (shippingMethod === "local_pickup") {
91
+ shippingNum = 0.0;
92
+ }
93
+
94
+ // Tax (10% of subtotal after discounts)
95
+ const taxableBase = subtotalNum - discountNum;
96
+ const taxNum = taxableBase > 0 ? taxableBase * 0.1 : 0.0;
97
+
98
+ const totalNum = subtotalNum - discountNum + shippingNum + taxNum;
99
+
100
+ return {
101
+ subtotal: subtotalNum.toFixed(2),
102
+ discount: discountNum.toFixed(2),
103
+ shipping: shippingNum.toFixed(2),
104
+ tax: taxNum.toFixed(2),
105
+ total: totalNum.toFixed(2)
106
+ };
107
+ };
108
+
109
+ const updateCartState = (newItems: CartItem[], newCoupons: string[], newMethod: string, newAddress: any) => {
110
+ const newTotals = recalculateTotals(newItems, newCoupons, newMethod);
111
+ const updatedCart = {
112
+ items: newItems,
113
+ totals: newTotals,
114
+ coupons: newCoupons,
115
+ shippingMethod: newMethod,
116
+ shippingAddress: newAddress
117
+ };
118
+ setCart(updatedCart);
119
+ if (typeof window !== "undefined") {
120
+ localStorage.setItem("forgewp-cart", JSON.stringify(updatedCart));
121
+ }
122
+ };
123
+
124
+ const addToCart = async (productId: number, quantity: number, variation?: Record<string, string>) => {
125
+ setIsLoading(true);
126
+
127
+ if (!IS_DEV) {
128
+ try {
129
+ const body: any = {
130
+ id: productId,
131
+ quantity
132
+ };
133
+ if (variation) {
134
+ body.variation = Object.entries(variation).map(([name, value]) => ({
135
+ attribute: name.startsWith("attribute_") ? name : `attribute_${name}`,
136
+ value
137
+ }));
138
+ }
139
+
140
+ const storeCart = await fetchStoreApi("cart/add-item", {
141
+ method: "POST",
142
+ body: JSON.stringify(body)
143
+ });
144
+ setCart(mapStoreApiToCartState(storeCart));
145
+ } catch (err) {
146
+ console.error("Failed to add item to WooCommerce cart:", err);
147
+ } finally {
148
+ setIsLoading(false);
149
+ }
150
+ return;
151
+ }
152
+
153
+ await new Promise((resolve) => setTimeout(resolve, 800));
154
+
155
+ const product = products.find((p) => p.id === productId);
156
+ if (!product) {
157
+ setIsLoading(false);
158
+ return;
159
+ }
160
+
161
+ let itemPrice = parseFloat(product.price || "0");
162
+ let variationId = "";
163
+ let itemTitle = product.title;
164
+
165
+ if (product.type === "variable" && variation && product.variations) {
166
+ // Find matching variation
167
+ const match = product.variations.find((v) => {
168
+ return Object.entries(variation).every(([attr, val]) => v.attributes[attr] === val);
169
+ });
170
+ if (match) {
171
+ itemPrice = parseFloat(match.price);
172
+ variationId = String(match.id);
173
+ const desc = Object.entries(variation).map(([k, v]) => `${k}: ${v}`).join(", ");
174
+ itemTitle = `${product.title} (${desc})`;
175
+ }
176
+ }
177
+
178
+ const lineKey = variationId ? `${productId}_${variationId}` : `${productId}`;
179
+ const existingIndex = cart.items.findIndex((item) => item.key === lineKey);
180
+
181
+ const newItems = [...cart.items];
182
+ if (existingIndex > -1) {
183
+ const existing = newItems[existingIndex];
184
+ const newQty = existing.quantity + quantity;
185
+ newItems[existingIndex] = {
186
+ ...existing,
187
+ quantity: newQty,
188
+ line_subtotal: (newQty * parseFloat(existing.price)).toFixed(2)
189
+ };
190
+ } else {
191
+ newItems.push({
192
+ key: lineKey,
193
+ id: productId,
194
+ quantity,
195
+ variation,
196
+ title: itemTitle,
197
+ price: itemPrice.toFixed(2),
198
+ featuredImage: product.featuredImage || "https://picsum.photos/seed/placeholder/300/300",
199
+ line_subtotal: (quantity * itemPrice).toFixed(2)
200
+ });
201
+ }
202
+
203
+ updateCartState(newItems, cart.coupons, cart.shippingMethod, cart.shippingAddress);
204
+ setIsLoading(false);
205
+ };
206
+
207
+ const addToCartBatch = async (batchItems: Array<{ productId: number; quantity: number; variation?: Record<string, string> }>) => {
208
+ setIsLoading(true);
209
+
210
+ if (!IS_DEV) {
211
+ try {
212
+ let lastCart = null;
213
+ for (const item of batchItems) {
214
+ const body: any = {
215
+ id: item.productId,
216
+ quantity: item.quantity
217
+ };
218
+ if (item.variation) {
219
+ body.variation = Object.entries(item.variation).map(([name, value]) => ({
220
+ attribute: name.startsWith("attribute_") ? name : `attribute_${name}`,
221
+ value
222
+ }));
223
+ }
224
+ lastCart = await fetchStoreApi("cart/add-item", {
225
+ method: "POST",
226
+ body: JSON.stringify(body)
227
+ });
228
+ }
229
+ if (lastCart) {
230
+ setCart(mapStoreApiToCartState(lastCart));
231
+ }
232
+ } catch (err) {
233
+ console.error("Failed to add batch to WooCommerce cart:", err);
234
+ } finally {
235
+ setIsLoading(false);
236
+ }
237
+ return;
238
+ }
239
+
240
+ await new Promise((resolve) => setTimeout(resolve, 1000));
241
+
242
+ const newItems = [...cart.items];
243
+
244
+ batchItems.forEach((b) => {
245
+ const product = products.find((p) => p.id === b.productId);
246
+ if (!product) return;
247
+
248
+ let itemPrice = parseFloat(product.price || "0");
249
+ let variationId = "";
250
+ let itemTitle = product.title;
251
+
252
+ if (product.type === "variable" && b.variation && product.variations) {
253
+ const match = product.variations.find((v) => {
254
+ return Object.entries(b.variation!).every(([attr, val]) => v.attributes[attr] === val);
255
+ });
256
+ if (match) {
257
+ itemPrice = parseFloat(match.price);
258
+ variationId = String(match.id);
259
+ const desc = Object.entries(b.variation).map(([k, v]) => `${k}: ${v}`).join(", ");
260
+ itemTitle = `${product.title} (${desc})`;
261
+ }
262
+ }
263
+
264
+ const lineKey = variationId ? `${b.productId}_${variationId}` : `${b.productId}`;
265
+ const existingIndex = newItems.findIndex((item) => item.key === lineKey);
266
+
267
+ if (existingIndex > -1) {
268
+ const existing = newItems[existingIndex];
269
+ const newQty = existing.quantity + b.quantity;
270
+ newItems[existingIndex] = {
271
+ ...existing,
272
+ quantity: newQty,
273
+ line_subtotal: (newQty * parseFloat(existing.price)).toFixed(2)
274
+ };
275
+ } else {
276
+ newItems.push({
277
+ key: lineKey,
278
+ id: b.productId,
279
+ quantity: b.quantity,
280
+ variation: b.variation,
281
+ title: itemTitle,
282
+ price: itemPrice.toFixed(2),
283
+ featuredImage: product.featuredImage || "https://picsum.photos/seed/placeholder/300/300",
284
+ line_subtotal: (b.quantity * itemPrice).toFixed(2)
285
+ });
286
+ }
287
+ });
288
+
289
+ updateCartState(newItems, cart.coupons, cart.shippingMethod, cart.shippingAddress);
290
+ setIsLoading(false);
291
+ };
292
+
293
+ const updateQuantity = async (key: string, qty: number) => {
294
+ if (qty < 1) {
295
+ await removeItem(key);
296
+ return;
297
+ }
298
+ setIsLoading(true);
299
+
300
+ if (!IS_DEV) {
301
+ try {
302
+ const storeCart = await fetchStoreApi("cart/update-item", {
303
+ method: "POST",
304
+ body: JSON.stringify({
305
+ key,
306
+ quantity: qty
307
+ })
308
+ });
309
+ setCart(mapStoreApiToCartState(storeCart));
310
+ } catch (err) {
311
+ console.error("Failed to update item quantity in WooCommerce cart:", err);
312
+ } finally {
313
+ setIsLoading(false);
314
+ }
315
+ return;
316
+ }
317
+
318
+ await new Promise((resolve) => setTimeout(resolve, 300));
319
+
320
+ const newItems = cart.items.map((item) => {
321
+ if (item.key === key) {
322
+ return {
323
+ ...item,
324
+ quantity: qty,
325
+ line_subtotal: (qty * parseFloat(item.price)).toFixed(2)
326
+ };
327
+ }
328
+ return item;
329
+ });
330
+
331
+ updateCartState(newItems, cart.coupons, cart.shippingMethod, cart.shippingAddress);
332
+ setIsLoading(false);
333
+ };
334
+
335
+ const removeItem = async (key: string) => {
336
+ setIsLoading(true);
337
+
338
+ if (!IS_DEV) {
339
+ try {
340
+ const storeCart = await fetchStoreApi("cart/remove-item", {
341
+ method: "POST",
342
+ body: JSON.stringify({
343
+ key
344
+ })
345
+ });
346
+ setCart(mapStoreApiToCartState(storeCart));
347
+ } catch (err) {
348
+ console.error("Failed to remove item from WooCommerce cart:", err);
349
+ } finally {
350
+ setIsLoading(false);
351
+ }
352
+ return;
353
+ }
354
+
355
+ await new Promise((resolve) => setTimeout(resolve, 400));
356
+ const newItems = cart.items.filter((item) => item.key !== key);
357
+ updateCartState(newItems, cart.coupons, cart.shippingMethod, cart.shippingAddress);
358
+ setIsLoading(false);
359
+ };
360
+
361
+ const applyCoupon = async (code: string) => {
362
+ setIsLoading(true);
363
+
364
+ if (!IS_DEV) {
365
+ try {
366
+ const storeCart = await fetchStoreApi("cart/apply-coupon", {
367
+ method: "POST",
368
+ body: JSON.stringify({
369
+ code
370
+ })
371
+ });
372
+ setCart(mapStoreApiToCartState(storeCart));
373
+ return true;
374
+ } catch (err) {
375
+ console.error("Failed to apply coupon to WooCommerce cart:", err);
376
+ return false;
377
+ } finally {
378
+ setIsLoading(false);
379
+ }
380
+ }
381
+
382
+ await new Promise((resolve) => setTimeout(resolve, 800));
383
+ const upperCode = code.toUpperCase();
384
+ if (upperCode === "BRUTAL5" || upperCode === "STARK") {
385
+ if (!cart.coupons.includes(upperCode)) {
386
+ updateCartState(cart.items, [...cart.coupons, upperCode], cart.shippingMethod, cart.shippingAddress);
387
+ setIsLoading(false);
388
+ return true;
389
+ }
390
+ }
391
+ setIsLoading(false);
392
+ return false;
393
+ };
394
+
395
+ const removeCoupon = async (code: string) => {
396
+ setIsLoading(true);
397
+
398
+ if (!IS_DEV) {
399
+ try {
400
+ const storeCart = await fetchStoreApi("cart/remove-coupon", {
401
+ method: "POST",
402
+ body: JSON.stringify({
403
+ code
404
+ })
405
+ });
406
+ setCart(mapStoreApiToCartState(storeCart));
407
+ } catch (err) {
408
+ console.error("Failed to remove coupon from WooCommerce cart:", err);
409
+ } finally {
410
+ setIsLoading(false);
411
+ }
412
+ return;
413
+ }
414
+
415
+ await new Promise((resolve) => setTimeout(resolve, 400));
416
+ const newCoupons = cart.coupons.filter((c) => c !== code);
417
+ updateCartState(cart.items, newCoupons, cart.shippingMethod, cart.shippingAddress);
418
+ setIsLoading(false);
419
+ };
420
+
421
+ const calculateShipping = async (address: { country: string; city: string; postcode: string }) => {
422
+ setIsLoading(true);
423
+
424
+ if (!IS_DEV) {
425
+ try {
426
+ const storeCart = await fetchStoreApi("cart/update-customer", {
427
+ method: "POST",
428
+ body: JSON.stringify({
429
+ shipping_address: address,
430
+ billing_address: address
431
+ })
432
+ });
433
+ setCart(mapStoreApiToCartState(storeCart));
434
+ } catch (err) {
435
+ console.error("Failed to update shipping calculation:", err);
436
+ } finally {
437
+ setIsLoading(false);
438
+ }
439
+ return;
440
+ }
441
+
442
+ await new Promise((resolve) => setTimeout(resolve, 700));
443
+ updateCartState(cart.items, cart.coupons, cart.shippingMethod, address);
444
+ setIsLoading(false);
445
+ };
446
+
447
+ const setShippingMethod = async (methodId: string) => {
448
+ if (!IS_DEV) {
449
+ setIsLoading(true);
450
+ try {
451
+ const storeCart = await fetchStoreApi("cart/select-shipping-rate", {
452
+ method: "POST",
453
+ body: JSON.stringify({
454
+ rate_id: methodId
455
+ })
456
+ });
457
+ setCart(mapStoreApiToCartState(storeCart));
458
+ } catch (err) {
459
+ console.error("Failed to select shipping method:", err);
460
+ } finally {
461
+ setIsLoading(false);
462
+ }
463
+ return;
464
+ }
465
+
466
+ updateCartState(cart.items, cart.coupons, methodId, cart.shippingAddress);
467
+ };
468
+
469
+ const clearCart = () => {
470
+ if (!IS_DEV) {
471
+ setCart({
472
+ items: [],
473
+ totals: { subtotal: "0.00", discount: "0.00", shipping: "0.00", tax: "0.00", total: "0.00" },
474
+ coupons: [],
475
+ shippingAddress: { country: "US", city: "", postcode: "" },
476
+ shippingMethod: "flat_rate"
477
+ });
478
+ return;
479
+ }
480
+
481
+ updateCartState([], [], "flat_rate", { country: "US", city: "", postcode: "" });
482
+ };
483
+
484
+ const itemCount = cart.items.reduce((sum, item) => sum + item.quantity, 0);
485
+ const cartTotal = `$${cart.totals.total}`;
486
+
487
+ return (
488
+ <WpCartContext.Provider
489
+ value={{
490
+ cart,
491
+ itemCount,
492
+ cartTotal,
493
+ addToCart,
494
+ addToCartBatch,
495
+ updateQuantity,
496
+ removeItem,
497
+ applyCoupon,
498
+ removeCoupon,
499
+ calculateShipping,
500
+ setShippingMethod,
501
+ isLoading,
502
+ clearCart
503
+ }}
504
+ >
505
+ {children}
506
+ </WpCartContext.Provider>
507
+ );
508
+ }
509
+
510
+ // ── 2. Catalog & Product Detail Components ──────────────────────────────────
511
+
512
+ export function WpProductGallery({ productId }: { productId: number }) {
513
+ const { product, loading } = useWpProduct(productId);
514
+ const [activeImage, setActiveImage] = React.useState("");
515
+
516
+ React.useEffect(() => {
517
+ if (product) {
518
+ setActiveImage(product.featuredImage);
519
+ }
520
+ }, [product]);
521
+
522
+ // Listen to variation selection events
523
+ React.useEffect(() => {
524
+ const handleVariation = (e: Event) => {
525
+ const variationDetail = (e as CustomEvent).detail;
526
+ if (variationDetail && variationDetail.image && variationDetail.image.url) {
527
+ setActiveImage(variationDetail.image.url);
528
+ }
529
+ };
530
+ window.addEventListener(`forgewp-variation-selected-${productId}`, handleVariation);
531
+ return () => {
532
+ window.removeEventListener(`forgewp-variation-selected-${productId}`, handleVariation);
533
+ };
534
+ }, [productId]);
535
+
536
+ if (loading) return <div className="border-4 border-black p-4 font-mono text-center">LOADING GALLERY...</div>;
537
+ if (!product) return <div className="border-4 border-black p-4 font-mono font-bold text-center">PRODUCT NOT FOUND</div>;
538
+
539
+ const galleryImages = product.images || [];
540
+
541
+ return (
542
+ <div className="space-y-4">
543
+ {/* Main Image Frame */}
544
+ <div className="border-4 border-black bg-white p-2 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] relative aspect-square overflow-hidden flex items-center justify-center">
545
+ {product.on_sale && (
546
+ <span className="absolute top-4 left-4 bg-yellow-400 border-2 border-black px-3 py-1 font-mono font-black text-xs uppercase z-10">
547
+ Sale
548
+ </span>
549
+ )}
550
+ <img src={activeImage} alt={product.title} className="max-w-full max-h-full object-cover" />
551
+ </div>
552
+
553
+ {/* Thumbnails */}
554
+ {galleryImages.length > 0 && (
555
+ <div className="grid grid-cols-4 gap-2">
556
+ <button
557
+ onClick={() => setActiveImage(product.featuredImage)}
558
+ className={`border-2 border-black p-1 bg-white aspect-square overflow-hidden flex items-center justify-center ${
559
+ activeImage === product.featuredImage ? "outline outline-4 outline-black" : ""
560
+ }`}
561
+ >
562
+ <img src={product.featuredImage} alt="Featured" className="max-w-full max-h-full object-cover" />
563
+ </button>
564
+ {galleryImages.map((img: any, idx: number) => (
565
+ <button
566
+ key={img.id || idx}
567
+ onClick={() => setActiveImage(img.url)}
568
+ className={`border-2 border-black p-1 bg-white aspect-square overflow-hidden flex items-center justify-center ${
569
+ activeImage === img.url ? "outline outline-4 outline-black" : ""
570
+ }`}
571
+ >
572
+ <img src={img.url} alt={`Gallery ${idx}`} className="max-w-full max-h-full object-cover" />
573
+ </button>
574
+ ))}
575
+ </div>
576
+ )}
577
+ </div>
578
+ );
579
+ }
580
+
581
+ export function WpProductPrice({ productId }: { productId: number }) {
582
+ const { product, loading } = useWpProduct(productId);
583
+ const [selectedPriceHtml, setSelectedPriceHtml] = React.useState<string | null>(null);
584
+
585
+ React.useEffect(() => {
586
+ const handleVariation = (e: Event) => {
587
+ const variationDetail = (e as CustomEvent).detail;
588
+ if (variationDetail && variationDetail.price) {
589
+ setSelectedPriceHtml(`$${parseFloat(variationDetail.price).toFixed(2)}`);
590
+ }
591
+ };
592
+ window.addEventListener(`forgewp-variation-selected-${productId}`, handleVariation);
593
+ return () => {
594
+ window.removeEventListener(`forgewp-variation-selected-${productId}`, handleVariation);
595
+ };
596
+ }, [productId]);
597
+
598
+ if (loading || !product) return null;
599
+
600
+ if (selectedPriceHtml) {
601
+ return <div className="font-mono text-2xl font-black">{selectedPriceHtml}</div>;
602
+ }
603
+
604
+ if (product.type === "variable" && product.variations && product.variations.length > 0) {
605
+ const prices = product.variations.map((v) => parseFloat(v.price));
606
+ const minPrice = Math.min(...prices).toFixed(2);
607
+ const maxPrice = Math.max(...prices).toFixed(2);
608
+ return (
609
+ <div className="font-mono text-2xl font-black">
610
+ ${minPrice} — ${maxPrice}
611
+ </div>
612
+ );
613
+ }
614
+
615
+ const hasDiscount = product.on_sale && product.regular_price && product.regular_price !== product.price;
616
+
617
+ return (
618
+ <div className="flex items-center gap-3 font-mono text-2xl font-black">
619
+ {hasDiscount && (
620
+ <span className="text-zinc-400 line-through text-lg">${parseFloat(product.regular_price!).toFixed(2)}</span>
621
+ )}
622
+ <span>${parseFloat(product.price).toFixed(2)}</span>
623
+ </div>
624
+ );
625
+ }
626
+
627
+ export function WpProductVariationSelector({ productId }: { productId: number }) {
628
+ const { product, loading } = useWpProduct(productId);
629
+ const [selections, setSelections] = React.useState<Record<string, string>>({});
630
+
631
+ if (loading || !product || product.type !== "variable" || !product.attributes) return null;
632
+
633
+ const handleSelect = (attrName: string, optionVal: string) => {
634
+ const next = { ...selections, [attrName]: optionVal };
635
+ setSelections(next);
636
+
637
+ // Dispatch selection change
638
+ if (product.variations) {
639
+ const matchedVariation = product.variations.find((v) => {
640
+ return Object.entries(next).every(([attr, val]) => v.attributes[attr] === val);
641
+ });
642
+ if (matchedVariation) {
643
+ window.dispatchEvent(
644
+ new CustomEvent(`forgewp-variation-selected-${productId}`, { detail: matchedVariation })
645
+ );
646
+ }
647
+ }
648
+ };
649
+
650
+ return (
651
+ <div className="space-y-4 font-mono">
652
+ {product.attributes.map((attr) => (
653
+ <div key={attr.name} className="space-y-2">
654
+ <span className="font-bold text-xs uppercase text-zinc-500">{attr.name}</span>
655
+ <div className="flex flex-wrap gap-2">
656
+ {attr.options.map((opt) => {
657
+ const isSelected = selections[attr.name] === opt;
658
+ return (
659
+ <button
660
+ key={opt}
661
+ onClick={() => handleSelect(attr.name, opt)}
662
+ className={`border-2 border-black px-4 py-2 text-xs font-black uppercase transition-all shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] ${
663
+ isSelected ? "bg-black text-white translate-x-[1px] translate-y-[1px] shadow-none" : "bg-white text-black hover:bg-zinc-100"
664
+ }`}
665
+ >
666
+ {opt}
667
+ </button>
668
+ );
669
+ })}
670
+ </div>
671
+ </div>
672
+ ))}
673
+ </div>
674
+ );
675
+ }
676
+
677
+ export function WpProductStockStatus({ productId }: { productId: number }) {
678
+ const { product, loading } = useWpProduct(productId);
679
+ const [status, setStatus] = React.useState("instock");
680
+
681
+ React.useEffect(() => {
682
+ if (product) {
683
+ setStatus(product.stock_status || "instock");
684
+ }
685
+ }, [product]);
686
+
687
+ React.useEffect(() => {
688
+ const handleVariation = (e: Event) => {
689
+ const variationDetail = (e as CustomEvent).detail;
690
+ if (variationDetail && variationDetail.stock_status) {
691
+ setStatus(variationDetail.stock_status);
692
+ }
693
+ };
694
+ window.addEventListener(`forgewp-variation-selected-${productId}`, handleVariation);
695
+ return () => {
696
+ window.removeEventListener(`forgewp-variation-selected-${productId}`, handleVariation);
697
+ };
698
+ }, [productId]);
699
+
700
+ if (loading || !product) return null;
701
+
702
+ const isInstock = status === "instock";
703
+ const label = isInstock ? "In Stock" : status === "outofstock" ? "Out of Stock" : "On Backorder";
704
+
705
+ return (
706
+ <div className="font-mono text-xs uppercase flex items-center gap-2">
707
+ <span className={`w-3 h-3 border-2 border-black ${isInstock ? "bg-emerald-400" : "bg-red-500"}`} />
708
+ <span className="font-bold">{label}</span>
709
+ </div>
710
+ );
711
+ }
712
+
713
+ export function WpRelatedProducts({ productId, limit = 4 }: { productId: number; limit?: number }) {
714
+ const { product: targetProduct, loading: loadingTarget } = useWpProduct(productId);
715
+ const { products, loading: loadingAll } = useWpProducts();
716
+
717
+ if (loadingTarget || loadingAll || !targetProduct) return null;
718
+
719
+ const targetCategory = targetProduct._terms?.product_cat?.[0]?.slug;
720
+ const related = products
721
+ .filter((p) => p.id !== productId && p._terms?.product_cat?.some((cat) => cat.slug === targetCategory))
722
+ .slice(0, limit);
723
+
724
+ if (related.length === 0) return null;
725
+
726
+ return (
727
+ <div className="space-y-6">
728
+ <h2 className="font-mono font-black text-3xl uppercase tracking-wider border-b-4 border-black pb-2">
729
+ Related Products
730
+ </h2>
731
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
732
+ {related.map((p) => (
733
+ <div key={p.id} className="border-4 border-black p-4 bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] flex flex-col justify-between">
734
+ <div className="aspect-square border-2 border-black overflow-hidden flex items-center justify-center bg-zinc-50 mb-4">
735
+ <img src={p.featuredImage} alt={p.title} className="max-w-full max-h-full object-cover" />
736
+ </div>
737
+ <div>
738
+ <span className="font-mono font-bold text-xs uppercase text-zinc-400">
739
+ {p._terms?.product_cat?.[0]?.name}
740
+ </span>
741
+ <h3 className="font-mono font-black text-lg uppercase my-1 truncate">{p.title}</h3>
742
+ <div className="font-mono font-black text-md text-accent">
743
+ ${parseFloat(p.price || "0").toFixed(2)}
744
+ </div>
745
+ </div>
746
+ <a
747
+ href={`/product/${p.sku}`}
748
+ className="mt-4 border-4 border-black bg-black text-white text-center font-mono font-bold py-2 uppercase hover:bg-white hover:text-black transition-all block"
749
+ >
750
+ View Item
751
+ </a>
752
+ </div>
753
+ ))}
754
+ </div>
755
+ </div>
756
+ );
757
+ }
758
+
759
+ export function WpProductReviews({ productId }: { productId: number }) {
760
+ const { product, loading } = useWpProduct(productId);
761
+ const [reviews, setReviews] = React.useState<any[]>([]);
762
+ const [rating, setRating] = React.useState(5);
763
+
764
+ React.useEffect(() => {
765
+ if (product && product.reviews) {
766
+ setReviews(product.reviews);
767
+ }
768
+ }, [product]);
769
+
770
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
771
+ e.preventDefault();
772
+ const data = new FormData(e.currentTarget);
773
+ const author = data.get("author") as string;
774
+ const content = data.get("content") as string;
775
+
776
+ if (!author || !content) return;
777
+
778
+ if (!IS_DEV) {
779
+ try {
780
+ const body = new URLSearchParams();
781
+ body.append("comment_post_ID", String(productId));
782
+ body.append("author", author);
783
+ body.append("email", `${author.toLowerCase().replace(/\s+/g, "")}@example.com`);
784
+ body.append("comment", content);
785
+ body.append("rating", String(rating));
786
+
787
+ const response = await fetch("/wp-comments-post.php", {
788
+ method: "POST",
789
+ headers: {
790
+ "Content-Type": "application/x-www-form-urlencoded"
791
+ },
792
+ body: body.toString()
793
+ });
794
+
795
+ if (response.ok || response.redirected) {
796
+ const newRev = {
797
+ id: Date.now(),
798
+ author,
799
+ content,
800
+ rating,
801
+ date: new Date().toISOString().split("T")[0]
802
+ };
803
+ setReviews([newRev, ...reviews]);
804
+ e.currentTarget.reset();
805
+ } else {
806
+ console.error("Failed to submit WooCommerce review");
807
+ }
808
+ } catch (err) {
809
+ console.error("Error submitting review:", err);
810
+ }
811
+ return;
812
+ }
813
+
814
+ const newRev = {
815
+ id: Date.now(),
816
+ author,
817
+ content,
818
+ rating,
819
+ date: new Date().toISOString().split("T")[0]
820
+ };
821
+
822
+ const next = [newRev, ...reviews];
823
+ setReviews(next);
824
+ e.currentTarget.reset();
825
+ };
826
+
827
+ if (loading || !product) return null;
828
+
829
+ return (
830
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8 font-mono border-t-4 border-black pt-8">
831
+ {/* Review Feed */}
832
+ <div className="space-y-6">
833
+ <h3 className="font-black text-2xl uppercase border-b-4 border-black pb-2">
834
+ Customer Feedbacks ({reviews.length})
835
+ </h3>
836
+ {reviews.length === 0 ? (
837
+ <p className="text-zinc-500 italic text-sm">No reviews yet. Be the first to leave one!</p>
838
+ ) : (
839
+ <div className="space-y-4">
840
+ {reviews.map((r) => (
841
+ <div key={r.id} className="border-4 border-black p-4 bg-zinc-50 shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]">
842
+ <div className="flex justify-between items-center mb-2">
843
+ <span className="font-black text-sm uppercase">{r.author}</span>
844
+ <span className="text-xs text-zinc-500">{r.date}</span>
845
+ </div>
846
+ <div className="flex gap-1 mb-2 text-yellow-400">
847
+ {Array.from({ length: 5 }).map((_, i) => (
848
+ <Star key={i} size={14} className={i < r.rating ? "fill-current" : "text-zinc-300"} />
849
+ ))}
850
+ </div>
851
+ <p className="text-xs font-semibold leading-relaxed text-zinc-700">{r.content}</p>
852
+ </div>
853
+ ))}
854
+ </div>
855
+ )}
856
+ </div>
857
+
858
+ {/* Review Form Island */}
859
+ <div className="border-4 border-black p-6 bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] self-start">
860
+ <h3 className="font-black text-xl uppercase mb-4 border-b-2 border-black pb-2">Add a Review</h3>
861
+ <form onSubmit={handleSubmit} className="space-y-4">
862
+ <div className="space-y-1">
863
+ <label className="text-xs font-bold uppercase text-zinc-500">Rating</label>
864
+ <div className="flex gap-2 text-yellow-400">
865
+ {Array.from({ length: 5 }).map((_, i) => (
866
+ <button
867
+ key={i}
868
+ type="button"
869
+ onClick={() => setRating(i + 1)}
870
+ className="focus:outline-none"
871
+ >
872
+ <Star size={24} className={i < rating ? "fill-current" : "text-zinc-300"} />
873
+ </button>
874
+ ))}
875
+ </div>
876
+ </div>
877
+ <div className="space-y-1">
878
+ <label className="text-xs font-bold uppercase text-zinc-500">Your Name</label>
879
+ <input
880
+ name="author"
881
+ required
882
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none focus:ring-0 focus:border-black"
883
+ />
884
+ </div>
885
+ <div className="space-y-1">
886
+ <label className="text-xs font-bold uppercase text-zinc-500">Your Feedback</label>
887
+ <textarea
888
+ name="content"
889
+ required
890
+ rows={4}
891
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none focus:ring-0 focus:border-black"
892
+ />
893
+ </div>
894
+ <button
895
+ type="submit"
896
+ className="w-full border-4 border-black bg-black text-white font-bold py-3 uppercase text-xs hover:bg-white hover:text-black transition-all"
897
+ >
898
+ Submit Feedback
899
+ </button>
900
+ </form>
901
+ </div>
902
+ </div>
903
+ );
904
+ }
905
+
906
+ // ── 3. Shopping Cart Components ─────────────────────────────────────────────
907
+
908
+ export function WpCartLineItems() {
909
+ const { cart, updateQuantity, removeItem, isLoading } = useWpCart();
910
+
911
+ if (cart.items.length === 0) {
912
+ return (
913
+ <div className="border-4 border-black p-8 font-mono text-center font-bold bg-white uppercase">
914
+ Your Cart is empty.
915
+ </div>
916
+ );
917
+ }
918
+
919
+ return (
920
+ <div className="space-y-4 font-mono">
921
+ {cart.items.map((item) => (
922
+ <div
923
+ key={item.key}
924
+ className="border-4 border-black p-4 bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] flex flex-col sm:flex-row gap-4 items-center justify-between"
925
+ >
926
+ {/* Thumb and title */}
927
+ <div className="flex gap-4 items-center w-full sm:w-auto">
928
+ <div className="w-16 h-16 border-2 border-black overflow-hidden flex-shrink-0 flex items-center justify-center bg-zinc-50">
929
+ <img src={item.featuredImage} alt={item.title} className="object-cover w-full h-full" />
930
+ </div>
931
+ <div>
932
+ <h4 className="font-black text-sm uppercase leading-tight truncate max-w-[200px]">{item.title}</h4>
933
+ <span className="text-xs font-bold text-accent">${item.price}</span>
934
+ </div>
935
+ </div>
936
+
937
+ {/* Qty and price adjustments */}
938
+ <div className="flex gap-6 items-center w-full sm:w-auto justify-between sm:justify-end">
939
+ <div className="flex items-center border-2 border-black">
940
+ <button
941
+ disabled={isLoading}
942
+ onClick={() => updateQuantity(item.key, item.quantity - 1)}
943
+ className="p-2 hover:bg-zinc-100 border-r-2 border-black disabled:opacity-50"
944
+ >
945
+ <Minus size={12} />
946
+ </button>
947
+ <span className="px-4 text-xs font-bold">{item.quantity}</span>
948
+ <button
949
+ disabled={isLoading}
950
+ onClick={() => updateQuantity(item.key, item.quantity + 1)}
951
+ className="p-2 hover:bg-zinc-100 border-l-2 border-black disabled:opacity-50"
952
+ >
953
+ <Plus size={12} />
954
+ </button>
955
+ </div>
956
+
957
+ <div className="font-black text-md min-w-[70px] text-right">
958
+ ${parseFloat(item.line_subtotal).toFixed(2)}
959
+ </div>
960
+
961
+ <button
962
+ disabled={isLoading}
963
+ onClick={() => removeItem(item.key)}
964
+ className="text-red-500 hover:text-black disabled:opacity-50"
965
+ >
966
+ <Trash2 size={18} />
967
+ </button>
968
+ </div>
969
+ </div>
970
+ ))}
971
+ </div>
972
+ );
973
+ }
974
+
975
+ export function WpCartCouponForm() {
976
+ const { cart, applyCoupon, removeCoupon, isLoading } = useWpCart();
977
+ const [success, setSuccess] = React.useState<boolean | null>(null);
978
+
979
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
980
+ e.preventDefault();
981
+ const data = new FormData(e.currentTarget);
982
+ const code = data.get("coupon_code") as string;
983
+ if (!code) return;
984
+
985
+ const applied = await applyCoupon(code);
986
+ setSuccess(applied);
987
+ if (applied) {
988
+ e.currentTarget.reset();
989
+ }
990
+ };
991
+
992
+ return (
993
+ <div className="border-4 border-black p-4 bg-zinc-50 font-mono space-y-4">
994
+ <h4 className="font-black text-sm uppercase border-b-2 border-black pb-1">Promotional Coupons</h4>
995
+ <form onSubmit={handleSubmit} className="flex gap-2">
996
+ <input
997
+ name="coupon_code"
998
+ placeholder="COUPON CODE"
999
+ required
1000
+ className="border-2 border-black p-2 text-xs uppercase flex-1 focus:outline-none focus:ring-0"
1001
+ />
1002
+ <button
1003
+ type="submit"
1004
+ disabled={isLoading}
1005
+ className="bg-black text-white border-2 border-black px-4 py-2 text-xs font-black uppercase hover:bg-white hover:text-black transition-all"
1006
+ >
1007
+ Apply
1008
+ </button>
1009
+ </form>
1010
+
1011
+ {success === false && (
1012
+ <p className="text-red-500 text-xs font-bold uppercase">Invalid Code. Try "BRUTAL5" or "STARK"</p>
1013
+ )}
1014
+
1015
+ {cart.coupons.length > 0 && (
1016
+ <div className="space-y-2 pt-2 border-t border-zinc-200">
1017
+ <span className="text-[10px] font-bold text-zinc-500 uppercase">Active Coupons:</span>
1018
+ <div className="flex flex-wrap gap-2">
1019
+ {cart.coupons.map((c) => (
1020
+ <span
1021
+ key={c}
1022
+ className="bg-yellow-300 border-2 border-black px-2 py-1 text-[10px] font-black uppercase flex items-center gap-2"
1023
+ >
1024
+ {c}
1025
+ <button type="button" onClick={() => removeCoupon(c)} className="hover:text-red-500">
1026
+ <X size={10} />
1027
+ </button>
1028
+ </span>
1029
+ ))}
1030
+ </div>
1031
+ </div>
1032
+ )}
1033
+ </div>
1034
+ );
1035
+ }
1036
+
1037
+ export function WpCartShippingCalculator() {
1038
+ const { cart, calculateShipping, isLoading } = useWpCart();
1039
+
1040
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
1041
+ e.preventDefault();
1042
+ const data = new FormData(e.currentTarget);
1043
+ calculateShipping({
1044
+ country: data.get("country") as string,
1045
+ city: data.get("city") as string,
1046
+ postcode: data.get("postcode") as string
1047
+ });
1048
+ };
1049
+
1050
+ return (
1051
+ <div className="border-4 border-black p-4 bg-zinc-50 font-mono space-y-4">
1052
+ <h4 className="font-black text-sm uppercase border-b-2 border-black pb-1">Estimate Shipping</h4>
1053
+ <form onSubmit={handleSubmit} className="space-y-2">
1054
+ <input
1055
+ name="country"
1056
+ defaultValue={cart.shippingAddress.country}
1057
+ placeholder="Country (e.g. US)"
1058
+ required
1059
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1060
+ />
1061
+ <input
1062
+ name="city"
1063
+ defaultValue={cart.shippingAddress.city}
1064
+ placeholder="City"
1065
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1066
+ />
1067
+ <input
1068
+ name="postcode"
1069
+ defaultValue={cart.shippingAddress.postcode}
1070
+ placeholder="Postcode"
1071
+ required
1072
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1073
+ />
1074
+ <button
1075
+ type="submit"
1076
+ disabled={isLoading}
1077
+ className="w-full bg-black text-white border-2 border-black py-2 text-xs font-black uppercase hover:bg-white hover:text-black transition-all"
1078
+ >
1079
+ Update Destination
1080
+ </button>
1081
+ </form>
1082
+ </div>
1083
+ );
1084
+ }
1085
+
1086
+ // ── 4. Checkout Components ──────────────────────────────────────────────────
1087
+
1088
+ export const CheckoutFormContext = React.createContext<{
1089
+ billing: Record<string, string>;
1090
+ setBilling: React.Dispatch<React.SetStateAction<Record<string, string>>>;
1091
+ shipping: Record<string, string>;
1092
+ setShipping: React.Dispatch<React.SetStateAction<Record<string, string>>>;
1093
+ selectedGateway: string;
1094
+ setSelectedGateway: (id: string) => void;
1095
+ } | null>(null);
1096
+
1097
+ export function WpCheckoutForm({
1098
+ children,
1099
+ className
1100
+ }: {
1101
+ children: React.ReactNode;
1102
+ className?: string;
1103
+ }) {
1104
+ const [billing, setBilling] = React.useState<Record<string, string>>({});
1105
+ const [shipping, setShipping] = React.useState<Record<string, string>>({});
1106
+ const [selectedGateway, setSelectedGateway] = React.useState("stripe");
1107
+
1108
+ return (
1109
+ <CheckoutFormContext.Provider
1110
+ value={{
1111
+ billing,
1112
+ setBilling,
1113
+ shipping,
1114
+ setShipping,
1115
+ selectedGateway,
1116
+ setSelectedGateway
1117
+ }}
1118
+ >
1119
+ <div className={className}>{children}</div>
1120
+ </CheckoutFormContext.Provider>
1121
+ );
1122
+ }
1123
+
1124
+ WpCheckoutForm.BillingFields = function BillingFields({ className }: { className?: string }) {
1125
+ const context = React.useContext(CheckoutFormContext);
1126
+ if (!context) return null;
1127
+
1128
+ const handleChange = (name: string, val: string) => {
1129
+ context.setBilling((prev) => ({ ...prev, [name]: val }));
1130
+ };
1131
+
1132
+ return (
1133
+ <div className={`space-y-4 font-mono ${className}`}>
1134
+ <div className="grid grid-cols-2 gap-4">
1135
+ <div className="space-y-1">
1136
+ <label className="text-[10px] font-bold uppercase text-zinc-500">First Name</label>
1137
+ <input
1138
+ required
1139
+ onChange={(e) => handleChange("first_name", e.target.value)}
1140
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1141
+ />
1142
+ </div>
1143
+ <div className="space-y-1">
1144
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Last Name</label>
1145
+ <input
1146
+ required
1147
+ onChange={(e) => handleChange("last_name", e.target.value)}
1148
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1149
+ />
1150
+ </div>
1151
+ </div>
1152
+ <div className="space-y-1">
1153
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Email Address</label>
1154
+ <input
1155
+ type="email"
1156
+ required
1157
+ onChange={(e) => handleChange("email", e.target.value)}
1158
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1159
+ />
1160
+ </div>
1161
+ <div className="space-y-1">
1162
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Street Address</label>
1163
+ <input
1164
+ required
1165
+ placeholder="House number and street name"
1166
+ onChange={(e) => handleChange("address_1", e.target.value)}
1167
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1168
+ />
1169
+ </div>
1170
+ <div className="grid grid-cols-2 gap-4">
1171
+ <div className="space-y-1">
1172
+ <label className="text-[10px] font-bold uppercase text-zinc-500">City</label>
1173
+ <input
1174
+ required
1175
+ onChange={(e) => handleChange("city", e.target.value)}
1176
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1177
+ />
1178
+ </div>
1179
+ <div className="space-y-1">
1180
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Postcode</label>
1181
+ <input
1182
+ required
1183
+ onChange={(e) => handleChange("postcode", e.target.value)}
1184
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1185
+ />
1186
+ </div>
1187
+ </div>
1188
+ <div className="space-y-1">
1189
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Country</label>
1190
+ <input
1191
+ required
1192
+ defaultValue="US"
1193
+ onChange={(e) => handleChange("country", e.target.value)}
1194
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1195
+ />
1196
+ </div>
1197
+ </div>
1198
+ );
1199
+ };
1200
+
1201
+ WpCheckoutForm.PaymentGateways = function WpPaymentGateways({ className }: { className?: string }) {
1202
+ const context = React.useContext(CheckoutFormContext);
1203
+ const { paymentGateways } = useWpCheckout();
1204
+ if (!context) return null;
1205
+
1206
+ return (
1207
+ <div className={`space-y-4 font-mono ${className}`}>
1208
+ {paymentGateways.map((g) => {
1209
+ const isSelected = context.selectedGateway === g.id;
1210
+ return (
1211
+ <label
1212
+ key={g.id}
1213
+ onClick={() => context.setSelectedGateway(g.id)}
1214
+ className={`border-4 border-black p-4 bg-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] cursor-pointer flex gap-3 items-start select-none ${
1215
+ isSelected ? "outline outline-4 outline-black" : ""
1216
+ }`}
1217
+ >
1218
+ <div className={`w-4 h-4 border-2 border-black mt-1 flex-shrink-0 flex items-center justify-center`}>
1219
+ {isSelected && <div className="w-2 h-2 bg-black" />}
1220
+ </div>
1221
+ <div>
1222
+ <span className="font-black text-sm uppercase leading-tight">{g.title}</span>
1223
+ <p className="text-[10px] text-zinc-500 mt-1 font-semibold leading-relaxed">{g.description}</p>
1224
+ {g.id === "stripe" && isSelected && (
1225
+ <div className="mt-4 space-y-2 border-t-2 border-dashed border-zinc-200 pt-4 w-full">
1226
+ <span className="text-[10px] font-bold text-zinc-500 uppercase">Secure Card Fields</span>
1227
+ <input
1228
+ placeholder="CARD NUMBER"
1229
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1230
+ />
1231
+ <div className="grid grid-cols-2 gap-2">
1232
+ <input
1233
+ placeholder="MM/YY"
1234
+ className="border-2 border-black p-2 text-xs focus:outline-none"
1235
+ />
1236
+ <input
1237
+ placeholder="CVC"
1238
+ className="border-2 border-black p-2 text-xs focus:outline-none"
1239
+ />
1240
+ </div>
1241
+ </div>
1242
+ )}
1243
+ </div>
1244
+ </label>
1245
+ );
1246
+ })}
1247
+ </div>
1248
+ );
1249
+ };
1250
+
1251
+ WpCheckoutForm.SubmitButton = function SubmitButton({ className }: { className?: string }) {
1252
+ const context = React.useContext(CheckoutFormContext);
1253
+ const { processOrder, isSubmitting, error } = useWpCheckout();
1254
+
1255
+ if (!context) return null;
1256
+
1257
+ const handleOrder = async () => {
1258
+ const res = await processOrder(context.billing, context.shipping, context.selectedGateway);
1259
+ if (res && res.success && res.redirectUrl) {
1260
+ if (typeof window !== "undefined") {
1261
+ window.location.href = res.redirectUrl + `?order_id=${res.orderId}`;
1262
+ }
1263
+ }
1264
+ };
1265
+
1266
+ return (
1267
+ <div className="space-y-4">
1268
+ {error && (
1269
+ <div className="border-4 border-red-500 bg-red-50 text-red-700 p-4 font-mono font-bold text-xs uppercase">
1270
+ ❌ {error}
1271
+ </div>
1272
+ )}
1273
+ <button
1274
+ onClick={handleOrder}
1275
+ disabled={isSubmitting}
1276
+ className={`w-full text-center font-mono font-black py-4 uppercase border-4 border-black bg-black text-white hover:bg-white hover:text-black transition-all shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] disabled:opacity-50 ${className}`}
1277
+ >
1278
+ {isSubmitting ? "Processing Order..." : "Place Order"}
1279
+ </button>
1280
+ </div>
1281
+ );
1282
+ };
1283
+
1284
+ export function WpShippingSelector() {
1285
+ const { shippingMethods } = useWpCheckout();
1286
+ const { cart, setShippingMethod } = useWpCart();
1287
+
1288
+ return (
1289
+ <div className="space-y-4 font-mono">
1290
+ {shippingMethods.map((m) => {
1291
+ const isSelected = cart.shippingMethod === m.id;
1292
+ return (
1293
+ <label
1294
+ key={m.id}
1295
+ onClick={() => setShippingMethod(m.id)}
1296
+ className={`border-4 border-black p-4 bg-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] cursor-pointer flex gap-3 items-start select-none ${
1297
+ isSelected ? "outline outline-4 outline-black" : ""
1298
+ }`}
1299
+ >
1300
+ <div className={`w-4 h-4 border-2 border-black mt-1 flex-shrink-0 flex items-center justify-center`}>
1301
+ {isSelected && <div className="w-2 h-2 bg-black" />}
1302
+ </div>
1303
+ <div className="flex-1 flex justify-between items-center">
1304
+ <div>
1305
+ <span className="font-black text-sm uppercase leading-tight">{m.title}</span>
1306
+ <p className="text-[10px] text-zinc-500 mt-1 font-semibold leading-relaxed">{m.description}</p>
1307
+ </div>
1308
+ <span className="font-black text-md">${m.cost.toFixed(2)}</span>
1309
+ </div>
1310
+ </label>
1311
+ );
1312
+ })}
1313
+ </div>
1314
+ );
1315
+ }
1316
+
1317
+ // ── 5. Customer Accounts & Authentication ───────────────────────────────────
1318
+
1319
+ export function WpLoginForm() {
1320
+ const { login, loading } = useWpCustomer();
1321
+ const [error, setError] = React.useState<string | null>(null);
1322
+
1323
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
1324
+ e.preventDefault();
1325
+ setError(null);
1326
+ const data = new FormData(e.currentTarget);
1327
+ try {
1328
+ await login(data.get("email") as string, data.get("password") as string);
1329
+ } catch (err: any) {
1330
+ setError(err.message || "Failed to sign in");
1331
+ }
1332
+ };
1333
+
1334
+ return (
1335
+ <div className="border-4 border-black p-6 bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] font-mono max-w-sm mx-auto">
1336
+ <h3 className="font-black text-2xl uppercase mb-6 border-b-4 border-black pb-2 flex items-center gap-2">
1337
+ <Lock size={20} /> Sign In
1338
+ </h3>
1339
+ <form onSubmit={handleSubmit} className="space-y-4">
1340
+ {error && (
1341
+ <div className="border-2 border-red-500 bg-red-50 text-red-600 p-2 text-xs uppercase font-bold">
1342
+ {error}
1343
+ </div>
1344
+ )}
1345
+ <div className="space-y-1">
1346
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Email Address</label>
1347
+ <input
1348
+ type="email"
1349
+ name="email"
1350
+ required
1351
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1352
+ />
1353
+ </div>
1354
+ <div className="space-y-1">
1355
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Password</label>
1356
+ <input
1357
+ type="password"
1358
+ name="password"
1359
+ required
1360
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1361
+ />
1362
+ </div>
1363
+ <button
1364
+ type="submit"
1365
+ disabled={loading}
1366
+ className="w-full bg-black text-white font-bold py-3 uppercase border-2 border-black hover:bg-white hover:text-black transition-all disabled:opacity-50"
1367
+ >
1368
+ {loading ? "Authenticating..." : "Sign In"}
1369
+ </button>
1370
+ </form>
1371
+ </div>
1372
+ );
1373
+ }
1374
+
1375
+ export function WpRegisterForm() {
1376
+ const { register, loading } = useWpCustomer();
1377
+ const [error, setError] = React.useState<string | null>(null);
1378
+
1379
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
1380
+ e.preventDefault();
1381
+ setError(null);
1382
+ const data = new FormData(e.currentTarget);
1383
+ try {
1384
+ await register(
1385
+ data.get("username") as string,
1386
+ data.get("email") as string,
1387
+ data.get("password") as string
1388
+ );
1389
+ } catch (err: any) {
1390
+ setError(err.message || "Failed to register");
1391
+ }
1392
+ };
1393
+
1394
+ return (
1395
+ <div className="border-4 border-black p-6 bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] font-mono max-w-sm mx-auto">
1396
+ <h3 className="font-black text-2xl uppercase mb-6 border-b-4 border-black pb-2 flex items-center gap-2">
1397
+ <User size={20} /> Register
1398
+ </h3>
1399
+ <form onSubmit={handleSubmit} className="space-y-4">
1400
+ {error && (
1401
+ <div className="border-2 border-red-500 bg-red-50 text-red-600 p-2 text-xs uppercase font-bold">
1402
+ {error}
1403
+ </div>
1404
+ )}
1405
+ <div className="space-y-1">
1406
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Username</label>
1407
+ <input
1408
+ name="username"
1409
+ required
1410
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1411
+ />
1412
+ </div>
1413
+ <div className="space-y-1">
1414
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Email Address</label>
1415
+ <input
1416
+ type="email"
1417
+ name="email"
1418
+ required
1419
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1420
+ />
1421
+ </div>
1422
+ <div className="space-y-1">
1423
+ <label className="text-[10px] font-bold uppercase text-zinc-500">Password</label>
1424
+ <input
1425
+ type="password"
1426
+ name="password"
1427
+ required
1428
+ className="w-full border-2 border-black p-2 text-xs focus:outline-none"
1429
+ />
1430
+ </div>
1431
+ <button
1432
+ type="submit"
1433
+ disabled={loading}
1434
+ className="w-full bg-black text-white font-bold py-3 uppercase border-2 border-black hover:bg-white hover:text-black transition-all disabled:opacity-50"
1435
+ >
1436
+ {loading ? "Registering..." : "Create Account"}
1437
+ </button>
1438
+ </form>
1439
+ </div>
1440
+ );
1441
+ }
1442
+
1443
+ export function WpOrderHistory() {
1444
+ const { orders } = useWpCustomer();
1445
+
1446
+ if (orders.length === 0) {
1447
+ return <div className="border-4 border-black p-4 font-mono text-center font-bold">NO ORDER HISTORY</div>;
1448
+ }
1449
+
1450
+ return (
1451
+ <div className="border-4 border-black overflow-x-auto font-mono bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
1452
+ <table className="w-full text-left border-collapse">
1453
+ <thead>
1454
+ <tr className="bg-black text-white text-xs uppercase">
1455
+ <th className="p-4 border-r-2 border-black">Order ID</th>
1456
+ <th className="p-4 border-r-2 border-black">Date</th>
1457
+ <th className="p-4 border-r-2 border-black">Status</th>
1458
+ <th className="p-4 border-r-2 border-black">Total</th>
1459
+ <th className="p-4">Downloads</th>
1460
+ </tr>
1461
+ </thead>
1462
+ <tbody className="text-xs font-bold divide-y divide-zinc-200">
1463
+ {orders.map((o) => (
1464
+ <tr key={o.id} className="hover:bg-zinc-50">
1465
+ <td className="p-4 border-r-2 border-black font-black">#{o.id}</td>
1466
+ <td className="p-4 border-r-2 border-black">{o.date}</td>
1467
+ <td className="p-4 border-r-2 border-black">
1468
+ <span className="bg-emerald-200 border-2 border-black px-2 py-1 text-[10px] font-black uppercase">
1469
+ {o.status}
1470
+ </span>
1471
+ </td>
1472
+ <td className="p-4 border-r-2 border-black font-black">{o.total}</td>
1473
+ <td className="p-4">
1474
+ {o.downloadableFiles && o.downloadableFiles.length > 0 ? (
1475
+ <div className="space-y-1">
1476
+ {o.downloadableFiles.map((file) => (
1477
+ <a
1478
+ key={file.name}
1479
+ href={file.url}
1480
+ className="inline-flex items-center gap-1 text-black underline hover:text-accent"
1481
+ >
1482
+ <Download size={12} /> {file.name}
1483
+ </a>
1484
+ ))}
1485
+ </div>
1486
+ ) : (
1487
+ <span className="text-zinc-400 italic">None</span>
1488
+ )}
1489
+ </td>
1490
+ </tr>
1491
+ ))}
1492
+ </tbody>
1493
+ </table>
1494
+ </div>
1495
+ );
1496
+ }
1497
+
1498
+ // ── 6. Faceted Search & Filtering Components ────────────────────────────────
1499
+
1500
+ export function WpProductFilters() {
1501
+ const { activeFilters, setFilter, setPriceRange, setSortBy, resetFilters } = useWpProductFilters();
1502
+ const { products, loading } = useWpProducts();
1503
+ const categories = useWpProductCategories();
1504
+ const tags = useWpProductTags();
1505
+
1506
+ // Dynamically extract all attributes from variable products in mock database
1507
+ const attributes = React.useMemo(() => {
1508
+ const map: Record<string, Set<string>> = {};
1509
+ products.forEach((p) => {
1510
+ if (p.attributes) {
1511
+ p.attributes.forEach((attr: any) => {
1512
+ if (!map[attr.name]) map[attr.name] = new Set();
1513
+ attr.options.forEach((opt: string) => map[attr.name].add(opt));
1514
+ });
1515
+ }
1516
+ });
1517
+ const result: Record<string, string[]> = {};
1518
+ Object.entries(map).forEach(([k, v]) => {
1519
+ result[k] = Array.from(v);
1520
+ });
1521
+ return result;
1522
+ }, [products]);
1523
+
1524
+ const handleCheckboxChange = (type: string, val: string, checked: boolean) => {
1525
+ setFilter(type, val, checked);
1526
+ };
1527
+
1528
+ const activeFiltersCount =
1529
+ activeFilters.categories.length +
1530
+ activeFilters.tags.length +
1531
+ Object.values(activeFilters.attributes).reduce((sum, list) => sum + list.length, 0) +
1532
+ (activeFilters.priceRange[0] > 0 || activeFilters.priceRange[1] < 100 ? 1 : 0);
1533
+
1534
+ if (loading) return <div className="border-4 border-black p-4 font-mono text-center">LOADING FILTERS...</div>;
1535
+
1536
+ return (
1537
+ <div className="border-4 border-black p-6 bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] font-mono space-y-6 self-start">
1538
+ {/* Header and Reset */}
1539
+ <div className="flex justify-between items-center border-b-4 border-black pb-2">
1540
+ <span className="font-black text-lg uppercase flex items-center gap-2">
1541
+ <SlidersHorizontal size={18} /> Filters
1542
+ </span>
1543
+ {activeFiltersCount > 0 && (
1544
+ <button onClick={resetFilters} className="text-[10px] font-black uppercase underline hover:text-red-500">
1545
+ Reset
1546
+ </button>
1547
+ )}
1548
+ </div>
1549
+
1550
+ {/* Sort Widget */}
1551
+ <div className="space-y-2">
1552
+ <span className="text-xs font-bold uppercase text-zinc-500">Sort By</span>
1553
+ <select
1554
+ value={activeFilters.sortBy}
1555
+ onChange={(e) => setSortBy(e.target.value)}
1556
+ className="w-full border-2 border-black p-2 text-xs font-black uppercase bg-white focus:outline-none"
1557
+ >
1558
+ <option value="date">Default Sorting</option>
1559
+ <option value="price-asc">Price: Low to High</option>
1560
+ <option value="price-desc">Price: High to Low</option>
1561
+ <option value="title">Alphabetical</option>
1562
+ </select>
1563
+ </div>
1564
+
1565
+ {/* Categories Widget */}
1566
+ {categories.length > 0 && (
1567
+ <div className="space-y-2">
1568
+ <span className="text-xs font-bold uppercase text-zinc-500">Categories</span>
1569
+ <div className="space-y-1">
1570
+ {categories.map((cat: any) => {
1571
+ const isChecked = activeFilters.categories.includes(cat.slug);
1572
+ return (
1573
+ <label key={cat.slug} className="flex items-center gap-2 text-xs font-bold uppercase cursor-pointer select-none">
1574
+ <input
1575
+ type="checkbox"
1576
+ checked={isChecked}
1577
+ onChange={(e) => handleCheckboxChange("category", cat.slug, e.target.checked)}
1578
+ className="w-4 h-4 border-2 border-black rounded-none bg-white text-black focus:ring-0 focus:ring-offset-0"
1579
+ />
1580
+ {cat.name} ({cat.count || 0})
1581
+ </label>
1582
+ );
1583
+ })}
1584
+ </div>
1585
+ </div>
1586
+ )}
1587
+
1588
+ {/* Price Slider Range Widget */}
1589
+ <div className="space-y-2">
1590
+ <span className="text-xs font-bold uppercase text-zinc-500">Price Range</span>
1591
+ <div className="grid grid-cols-2 gap-2">
1592
+ <div className="flex items-center border-2 border-black p-1 text-xs font-bold uppercase">
1593
+ <span className="text-zinc-400 mr-1">$</span>
1594
+ <input
1595
+ type="number"
1596
+ value={activeFilters.priceRange[0]}
1597
+ onChange={(e) => setPriceRange(parseFloat(e.target.value) || 0, activeFilters.priceRange[1])}
1598
+ className="w-full border-0 p-0 text-xs font-black uppercase focus:ring-0"
1599
+ />
1600
+ </div>
1601
+ <div className="flex items-center border-2 border-black p-1 text-xs font-bold uppercase">
1602
+ <span className="text-zinc-400 mr-1">$</span>
1603
+ <input
1604
+ type="number"
1605
+ value={activeFilters.priceRange[1]}
1606
+ onChange={(e) => setPriceRange(activeFilters.priceRange[0], parseFloat(e.target.value) || 100)}
1607
+ className="w-full border-0 p-0 text-xs font-black uppercase focus:ring-0"
1608
+ />
1609
+ </div>
1610
+ </div>
1611
+ </div>
1612
+
1613
+ {/* Attribute Widgets */}
1614
+ {Object.entries(attributes).map(([name, options]) => (
1615
+ <div key={name} className="space-y-2 border-t-2 border-zinc-100 pt-4">
1616
+ <span className="text-xs font-bold uppercase text-zinc-500">{name}</span>
1617
+ <div className="space-y-1">
1618
+ {options.map((opt) => {
1619
+ const isChecked = (activeFilters.attributes[name] || []).includes(opt);
1620
+ return (
1621
+ <label key={opt} className="flex items-center gap-2 text-xs font-bold uppercase cursor-pointer select-none">
1622
+ <input
1623
+ type="checkbox"
1624
+ checked={isChecked}
1625
+ onChange={(e) => handleCheckboxChange(name, opt, e.target.checked)}
1626
+ className="w-4 h-4 border-2 border-black rounded-none bg-white text-black focus:ring-0"
1627
+ />
1628
+ {opt}
1629
+ </label>
1630
+ );
1631
+ })}
1632
+ </div>
1633
+ </div>
1634
+ ))}
1635
+
1636
+ {/* Tags Widget */}
1637
+ {tags.length > 0 && (
1638
+ <div className="space-y-2 border-t-2 border-zinc-100 pt-4">
1639
+ <span className="text-xs font-bold uppercase text-zinc-500">Popular Tags</span>
1640
+ <div className="flex flex-wrap gap-2">
1641
+ {tags.map((t: any) => {
1642
+ const isChecked = activeFilters.tags.includes(t.slug);
1643
+ return (
1644
+ <button
1645
+ key={t.slug}
1646
+ onClick={() => handleCheckboxChange("tag", t.slug, !isChecked)}
1647
+ className={`border-2 border-black px-2 py-1 text-[10px] font-black uppercase transition-all shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] ${
1648
+ isChecked ? "bg-black text-white translate-x-[1px] translate-y-[1px] shadow-none" : "bg-white text-black"
1649
+ }`}
1650
+ >
1651
+ {t.name}
1652
+ </button>
1653
+ );
1654
+ })}
1655
+ </div>
1656
+ </div>
1657
+ )}
1658
+ </div>
1659
+ );
1660
+ }
1661
+
1662
+ // ── 7. Wishlist / Favorites Components ──────────────────────────────────────
1663
+
1664
+ export function WpWishlistButton({ productId }: { productId: number }) {
1665
+ const { isWishlisted, toggleWishlist } = useWpWishlist();
1666
+ const wish = isWishlisted(productId);
1667
+
1668
+ return (
1669
+ <button
1670
+ onClick={() => toggleWishlist(productId)}
1671
+ className={`border-4 border-black p-3 bg-white shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:bg-zinc-100 transition-all ${
1672
+ wish ? "text-red-500 fill-current" : "text-black"
1673
+ }`}
1674
+ >
1675
+ <Heart size={18} className={wish ? "fill-current" : ""} />
1676
+ </button>
1677
+ );
1678
+ }
1679
+
1680
+ export function WpWishlistList() {
1681
+ const { wishlist, toggleWishlist } = useWpWishlist();
1682
+ const { products, loading } = useWpProducts();
1683
+ const wishItems = products.filter((p) => wishlist.includes(p.id));
1684
+
1685
+ if (loading) return <div className="border-4 border-black p-4 font-mono text-center">LOADING WISHLIST...</div>;
1686
+
1687
+ if (wishItems.length === 0) {
1688
+ return (
1689
+ <div className="border-4 border-black p-8 font-mono text-center font-bold bg-white uppercase">
1690
+ No items in your wishlist.
1691
+ </div>
1692
+ );
1693
+ }
1694
+
1695
+ return (
1696
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 font-mono">
1697
+ {wishItems.map((p) => (
1698
+ <div key={p.id} className="border-4 border-black p-4 bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] flex flex-col justify-between">
1699
+ <div className="aspect-square border-2 border-black overflow-hidden flex items-center justify-center bg-zinc-50 relative mb-4">
1700
+ <button
1701
+ onClick={() => toggleWishlist(p.id)}
1702
+ className="absolute top-2 right-2 border-2 border-black p-1 bg-white text-red-500 hover:bg-zinc-100"
1703
+ >
1704
+ <X size={14} />
1705
+ </button>
1706
+ <img src={p.featuredImage} alt={p.title} className="max-w-full max-h-full object-cover" />
1707
+ </div>
1708
+ <div>
1709
+ <h3 className="font-black text-lg uppercase truncate mb-1">{p.title}</h3>
1710
+ <span className="font-bold text-accent">${parseFloat(p.price || "0").toFixed(2)}</span>
1711
+ </div>
1712
+ <a
1713
+ href={`/product/${p.sku}`}
1714
+ className="mt-4 border-4 border-black bg-black text-white text-center font-bold py-2 uppercase hover:bg-white hover:text-black transition-all flex items-center justify-center gap-2"
1715
+ >
1716
+ View Details <ArrowRight size={14} />
1717
+ </a>
1718
+ </div>
1719
+ ))}
1720
+ </div>
1721
+ );
1722
+ }
1723
+
1724
+ // ── 8. Global Store Announcement Notices ────────────────────────────────────
1725
+
1726
+ export function WpStoreNotice() {
1727
+ const [visible, setVisible] = React.useState(true);
1728
+
1729
+ if (!visible) return null;
1730
+
1731
+ return (
1732
+ <div className="bg-yellow-300 border-b-4 border-black font-mono px-4 py-3 flex justify-between items-center gap-4 relative z-50">
1733
+ <div className="flex items-center gap-2 text-xs font-black uppercase">
1734
+ <span className="w-2.5 h-2.5 border-2 border-black bg-black animate-pulse" />
1735
+ This is a mock WooCommerce sandbox workspace. Orders will not be processed or charged.
1736
+ </div>
1737
+ <button onClick={() => setVisible(false)} className="border-2 border-black p-1 bg-white hover:bg-zinc-100">
1738
+ <X size={12} />
1739
+ </button>
1740
+ </div>
1741
+ );
1742
+ }
1743
+
1744
+
1745
+ export interface WpProductLoopProps {
1746
+ category?: string;
1747
+ limit?: number;
1748
+ orderBy?: string;
1749
+ order?: "asc" | "desc";
1750
+ status?: string;
1751
+ children: React.ReactNode;
1752
+ }
1753
+
1754
+ export function WpProductLoop({
1755
+ category,
1756
+ limit = 10,
1757
+ orderBy = "date",
1758
+ order = "desc",
1759
+ status = "publish",
1760
+ children
1761
+ }: WpProductLoopProps) {
1762
+ if (!IS_DEV) {
1763
+ return (
1764
+ <>
1765
+ {/* @ts-ignore */}
1766
+ <forgewp-product-loop-start
1767
+ category={category}
1768
+ limit={limit}
1769
+ orderBy={orderBy}
1770
+ order={order}
1771
+ status={status}
1772
+ />
1773
+ {children}
1774
+ {/* @ts-ignore */}
1775
+ <forgewp-product-loop-end />
1776
+ </>
1777
+ );
1778
+ }
1779
+
1780
+ const { products, loading } = useWpProducts();
1781
+
1782
+ const filtered = React.useMemo(() => {
1783
+ let items = [...products];
1784
+ if (category) {
1785
+ items = items.filter((p) => {
1786
+ const cats = p._terms?.product_cat || [];
1787
+ return cats.some(
1788
+ (c) =>
1789
+ c.slug.toLowerCase() === category.toLowerCase() ||
1790
+ c.name.toLowerCase() === category.toLowerCase()
1791
+ );
1792
+ });
1793
+ }
1794
+
1795
+ items.sort((a, b) => {
1796
+ let valA: any = (a as any)[orderBy] ?? "";
1797
+ let valB: any = (b as any)[orderBy] ?? "";
1798
+ if (orderBy === "date") {
1799
+ valA = a.id;
1800
+ valB = b.id;
1801
+ }
1802
+ if (order.toLowerCase() === "desc") {
1803
+ return valA < valB ? 1 : valA > valB ? -1 : 0;
1804
+ }
1805
+ return valA > valB ? 1 : valA < valB ? -1 : 0;
1806
+ });
1807
+
1808
+ return items.slice(0, limit);
1809
+ }, [products, category, limit, orderBy, order]);
1810
+
1811
+ if (loading) {
1812
+ return (
1813
+ <div className="border-4 border-black p-4 font-mono text-center">
1814
+ LOADING PRODUCTS...
1815
+ </div>
1816
+ );
1817
+ }
1818
+
1819
+ return (
1820
+ <>
1821
+ {filtered.map((prod) => {
1822
+ const postShape = {
1823
+ id: prod.id,
1824
+ title: prod.title,
1825
+ excerpt: prod.excerpt,
1826
+ content: prod.content,
1827
+ date: "",
1828
+ author: "Admin",
1829
+ featuredImage: prod.featuredImage,
1830
+ customFields: {
1831
+ price: prod.price,
1832
+ regular_price: prod.regular_price,
1833
+ on_sale: prod.on_sale,
1834
+ sku: prod.sku,
1835
+ stock_status: prod.stock_status,
1836
+ },
1837
+ __postType: "product",
1838
+ };
1839
+ return (
1840
+ <WpPostContext.Provider key={prod.id} value={postShape}>
1841
+ {children}
1842
+ </WpPostContext.Provider>
1843
+ );
1844
+ })}
1845
+ </>
1846
+ );
1847
+ }
1848
+