@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.
- package/package.json +28 -0
- package/src/components.tsx +1848 -0
- package/src/hooks.ts +825 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +16 -0
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { WpPostContext } from "@forgewp/react";
|
|
3
|
+
|
|
4
|
+
export const IS_DEV =
|
|
5
|
+
typeof import.meta !== 'undefined' &&
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
(import.meta as any).env?.DEV === true;
|
|
8
|
+
|
|
9
|
+
function getApiBaseUrl(): string {
|
|
10
|
+
if (typeof window !== 'undefined' && (window as any).FORGEWP_API_URL) {
|
|
11
|
+
return (window as any).FORGEWP_API_URL;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const viteUrl = (import.meta as any).env?.VITE_WP_API_URL;
|
|
15
|
+
if (viteUrl) return viteUrl;
|
|
16
|
+
} catch (e) {}
|
|
17
|
+
try {
|
|
18
|
+
if (typeof (globalThis as any).process !== 'undefined' && (globalThis as any).process.env) {
|
|
19
|
+
const nextUrl = (globalThis as any).process.env.NEXT_PUBLIC_WP_API_URL || (globalThis as any).process.env.WP_API_URL;
|
|
20
|
+
if (nextUrl) return nextUrl;
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
if (typeof window !== 'undefined') {
|
|
24
|
+
const homeUrl = (window as any).forgeWpHydration?.siteSettings?.options?.home;
|
|
25
|
+
if (homeUrl) {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(homeUrl).pathname.replace(/\/$/, '');
|
|
28
|
+
} catch (e) {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ProductType = "simple" | "variable" | "grouped" | "external";
|
|
35
|
+
|
|
36
|
+
export interface ProductVariation {
|
|
37
|
+
id: number;
|
|
38
|
+
attributes: Record<string, string>;
|
|
39
|
+
price: string;
|
|
40
|
+
regular_price: string;
|
|
41
|
+
stock_status: "instock" | "outofstock" | "onbackorder";
|
|
42
|
+
image?: { id?: number; url: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProductAttribute {
|
|
46
|
+
name: string;
|
|
47
|
+
options: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ProductReview {
|
|
51
|
+
id: number;
|
|
52
|
+
author: string;
|
|
53
|
+
content: string;
|
|
54
|
+
rating: number;
|
|
55
|
+
date: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WooCommerceProduct {
|
|
59
|
+
id: number;
|
|
60
|
+
type: ProductType;
|
|
61
|
+
title: string;
|
|
62
|
+
excerpt: string;
|
|
63
|
+
content: string;
|
|
64
|
+
price: string;
|
|
65
|
+
regular_price?: string;
|
|
66
|
+
on_sale?: boolean;
|
|
67
|
+
sku: string;
|
|
68
|
+
stock_status: "instock" | "outofstock" | "onbackorder";
|
|
69
|
+
featuredImage: string;
|
|
70
|
+
images?: Array<{ id?: number; url: string }>;
|
|
71
|
+
attributes?: ProductAttribute[];
|
|
72
|
+
variations?: ProductVariation[];
|
|
73
|
+
grouped_products?: number[];
|
|
74
|
+
external_url?: string;
|
|
75
|
+
button_text?: string;
|
|
76
|
+
virtual?: boolean;
|
|
77
|
+
downloadable?: boolean;
|
|
78
|
+
downloads?: Array<{ name: string; url: string }>;
|
|
79
|
+
average_rating: string;
|
|
80
|
+
rating_count: number;
|
|
81
|
+
reviews: ProductReview[];
|
|
82
|
+
_terms?: {
|
|
83
|
+
product_cat?: Array<{ id: number; slug: string; name: string }>;
|
|
84
|
+
product_tag?: Array<{ id: number; slug: string; name: string }>;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function useWpProducts(): { products: WooCommerceProduct[]; loading: boolean; error: string | null } {
|
|
89
|
+
const [products, setProducts] = React.useState<WooCommerceProduct[]>([]);
|
|
90
|
+
const [loading, setLoading] = React.useState(true);
|
|
91
|
+
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (typeof window !== "undefined") {
|
|
94
|
+
const mockProds = window._forgeWpMockPosts?.["product"] || [];
|
|
95
|
+
setProducts(mockProds);
|
|
96
|
+
setLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
return { products, loading, error: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function useWpProduct(productId: number | string): { product: WooCommerceProduct | null; loading: boolean; error: string | null } {
|
|
104
|
+
const { products, loading } = useWpProducts();
|
|
105
|
+
|
|
106
|
+
const product = React.useMemo(() => {
|
|
107
|
+
if (typeof productId === "number") {
|
|
108
|
+
return products.find((p) => p.id === productId) || null;
|
|
109
|
+
}
|
|
110
|
+
return products.find((p) => p.sku === productId) || null;
|
|
111
|
+
}, [products, productId]);
|
|
112
|
+
|
|
113
|
+
return { product, loading, error: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface WpProductCategory {
|
|
117
|
+
id: number;
|
|
118
|
+
name: string;
|
|
119
|
+
slug: string;
|
|
120
|
+
description: string;
|
|
121
|
+
count: number;
|
|
122
|
+
meta?: Record<string, any>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface WpProductTag {
|
|
126
|
+
id: number;
|
|
127
|
+
name: string;
|
|
128
|
+
slug: string;
|
|
129
|
+
count?: number;
|
|
130
|
+
meta?: Record<string, any>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function useWpProductCategories(): WpProductCategory[] {
|
|
134
|
+
const [categories, setCategories] = React.useState<WpProductCategory[]>([]);
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (typeof window !== "undefined") {
|
|
137
|
+
const mockCats = window._forgeWpMockPosts?.["_taxonomy_product_cat"] || [];
|
|
138
|
+
setCategories(mockCats);
|
|
139
|
+
}
|
|
140
|
+
}, []);
|
|
141
|
+
return categories;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function useWpProductTags(): WpProductTag[] {
|
|
145
|
+
const [tags, setTags] = React.useState<WpProductTag[]>([]);
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
if (typeof window !== "undefined") {
|
|
148
|
+
const mockTags = window._forgeWpMockPosts?.["_taxonomy_product_tag"] || [];
|
|
149
|
+
setTags(mockTags);
|
|
150
|
+
}
|
|
151
|
+
}, []);
|
|
152
|
+
return tags;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── 1. Shopping Cart Context & Hook ──────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export interface CartItem {
|
|
158
|
+
key: string;
|
|
159
|
+
id: number;
|
|
160
|
+
quantity: number;
|
|
161
|
+
variation?: Record<string, string>;
|
|
162
|
+
title: string;
|
|
163
|
+
price: string;
|
|
164
|
+
regular_price?: string;
|
|
165
|
+
featuredImage: string;
|
|
166
|
+
line_subtotal: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface CartTotals {
|
|
170
|
+
subtotal: string;
|
|
171
|
+
discount: string;
|
|
172
|
+
shipping: string;
|
|
173
|
+
tax: string;
|
|
174
|
+
total: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface CartState {
|
|
178
|
+
items: CartItem[];
|
|
179
|
+
totals: CartTotals;
|
|
180
|
+
coupons: string[];
|
|
181
|
+
shippingAddress: {
|
|
182
|
+
country: string;
|
|
183
|
+
city: string;
|
|
184
|
+
postcode: string;
|
|
185
|
+
};
|
|
186
|
+
shippingMethod: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let storeApiNonce =
|
|
190
|
+
typeof window !== "undefined" && typeof window.localStorage !== "undefined"
|
|
191
|
+
? window.localStorage.getItem("forgewp-store-nonce") || ""
|
|
192
|
+
: "";
|
|
193
|
+
|
|
194
|
+
export async function fetchStoreApi(endpoint: string, options: RequestInit = {}): Promise<any> {
|
|
195
|
+
const url = `${getApiBaseUrl()}/wp-json/wc/store/v1/${endpoint}`;
|
|
196
|
+
const headers = new Headers(options.headers || {});
|
|
197
|
+
|
|
198
|
+
if (storeApiNonce) {
|
|
199
|
+
headers.set("Nonce", storeApiNonce);
|
|
200
|
+
}
|
|
201
|
+
headers.set("Content-Type", "application/json");
|
|
202
|
+
|
|
203
|
+
if (typeof window !== "undefined") {
|
|
204
|
+
const token = window.localStorage.getItem("forgewp_jwt_token");
|
|
205
|
+
if (token) {
|
|
206
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const response = await fetch(url, {
|
|
211
|
+
...options,
|
|
212
|
+
headers,
|
|
213
|
+
credentials: "include",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const nextNonce = response.headers.get("Nonce");
|
|
217
|
+
if (nextNonce) {
|
|
218
|
+
storeApiNonce = nextNonce;
|
|
219
|
+
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
|
220
|
+
window.localStorage.setItem("forgewp-store-nonce", nextNonce);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
let errorData = null;
|
|
226
|
+
try {
|
|
227
|
+
errorData = await response.json();
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
throw new Error(errorData?.message || `Store API request failed with status ${response.status}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return response.json();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function mapStoreApiToCartState(storeCart: any): CartState {
|
|
236
|
+
const precision = storeCart.totals?.currency_minor_unit ?? 2;
|
|
237
|
+
const parseAmount = (val: string | number) => {
|
|
238
|
+
const num = typeof val === "number" ? val : parseInt(String(val || "0"), 10);
|
|
239
|
+
return (num / Math.pow(10, precision)).toFixed(precision);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const items = (storeCart.items || []).map((item: any) => {
|
|
243
|
+
let featuredImage = "https://picsum.photos/seed/placeholder/300/300";
|
|
244
|
+
if (item.images && item.images.length > 0) {
|
|
245
|
+
featuredImage = item.images[0].thumbnail || item.images[0].src || featuredImage;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const priceStr = parseAmount(item.prices?.price || 0);
|
|
249
|
+
const regularPriceStr = item.prices?.regular_price ? parseAmount(item.prices.regular_price) : undefined;
|
|
250
|
+
const lineSubtotalStr = parseAmount(item.totals?.line_subtotal || 0);
|
|
251
|
+
|
|
252
|
+
const variation: Record<string, string> = {};
|
|
253
|
+
if (Array.isArray(item.variation)) {
|
|
254
|
+
item.variation.forEach((v: any) => {
|
|
255
|
+
if (v.attribute && v.value) {
|
|
256
|
+
const name = v.attribute.replace(/^attribute_/, "");
|
|
257
|
+
variation[name] = v.value;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
key: item.key,
|
|
264
|
+
id: item.id,
|
|
265
|
+
quantity: item.quantity,
|
|
266
|
+
variation,
|
|
267
|
+
title: item.name,
|
|
268
|
+
price: priceStr,
|
|
269
|
+
regular_price: regularPriceStr,
|
|
270
|
+
featuredImage,
|
|
271
|
+
line_subtotal: lineSubtotalStr
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const totals = {
|
|
276
|
+
subtotal: parseAmount(storeCart.totals?.total_items || 0),
|
|
277
|
+
discount: parseAmount(storeCart.totals?.total_discount || 0),
|
|
278
|
+
shipping: parseAmount(storeCart.totals?.total_shipping || 0),
|
|
279
|
+
tax: parseAmount(storeCart.totals?.total_tax || 0),
|
|
280
|
+
total: parseAmount(storeCart.totals?.total_price || 0)
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const coupons = (storeCart.coupons || []).map((c: any) => c.code.toUpperCase());
|
|
284
|
+
|
|
285
|
+
const shippingAddress = {
|
|
286
|
+
country: storeCart.shipping_address?.country || "US",
|
|
287
|
+
city: storeCart.shipping_address?.city || "",
|
|
288
|
+
postcode: storeCart.shipping_address?.postcode || ""
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
let shippingMethod = "flat_rate";
|
|
292
|
+
if (storeCart.shipping_rates && storeCart.shipping_rates.length > 0) {
|
|
293
|
+
const pkg = storeCart.shipping_rates[0];
|
|
294
|
+
const selected = pkg.shipping_rates?.find((r: any) => r.selected);
|
|
295
|
+
if (selected) {
|
|
296
|
+
shippingMethod = selected.rate_id;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
items,
|
|
302
|
+
totals,
|
|
303
|
+
coupons,
|
|
304
|
+
shippingAddress,
|
|
305
|
+
shippingMethod
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export const WpCartContext = React.createContext<{
|
|
310
|
+
cart: CartState;
|
|
311
|
+
itemCount: number;
|
|
312
|
+
cartTotal: string;
|
|
313
|
+
addToCart: (productId: number, quantity: number, variation?: Record<string, string>) => Promise<void>;
|
|
314
|
+
addToCartBatch: (items: Array<{ productId: number; quantity: number; variation?: Record<string, string> }>) => Promise<void>;
|
|
315
|
+
updateQuantity: (key: string, qty: number) => Promise<void>;
|
|
316
|
+
removeItem: (key: string) => Promise<void>;
|
|
317
|
+
applyCoupon: (code: string) => Promise<boolean>;
|
|
318
|
+
removeCoupon: (code: string) => Promise<void>;
|
|
319
|
+
calculateShipping: (address: { country: string; city: string; postcode: string }) => Promise<void>;
|
|
320
|
+
setShippingMethod: (methodId: string) => void;
|
|
321
|
+
isLoading: boolean;
|
|
322
|
+
clearCart: () => void;
|
|
323
|
+
} | null>(null);
|
|
324
|
+
|
|
325
|
+
export function useWpCart() {
|
|
326
|
+
const context = React.useContext(WpCartContext);
|
|
327
|
+
if (!context) {
|
|
328
|
+
throw new Error("useWpCart must be used within a WpCartProvider");
|
|
329
|
+
}
|
|
330
|
+
return context;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── 2. Checkout Hook ─────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
export interface ShippingMethod {
|
|
336
|
+
id: string;
|
|
337
|
+
title: string;
|
|
338
|
+
cost: number;
|
|
339
|
+
description: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface PaymentGateway {
|
|
343
|
+
id: string;
|
|
344
|
+
title: string;
|
|
345
|
+
description: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function useWpCheckout() {
|
|
349
|
+
const cartContext = useWpCart();
|
|
350
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
351
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
352
|
+
|
|
353
|
+
const shippingMethods: ShippingMethod[] = [
|
|
354
|
+
{ id: "flat_rate", title: "Flat Rate Shipping", cost: 10.0, description: "Standard ground delivery (3-5 business days)" },
|
|
355
|
+
{ id: "free_shipping", title: "Free Shipping", cost: 0.0, description: "Available for orders over $100" },
|
|
356
|
+
{ id: "local_pickup", title: "Local Pickup", cost: 0.0, description: "Pickup from our warehouse location" }
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
const paymentGateways: PaymentGateway[] = [
|
|
360
|
+
{ id: "stripe", title: "Credit Card (Stripe)", description: "Pay securely with your credit card." },
|
|
361
|
+
{ id: "paypal", title: "PayPal", description: "Log in and pay using your PayPal account." },
|
|
362
|
+
{ id: "cod", title: "Cash on Delivery", description: "Pay with cash upon delivery of your order." }
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
const processOrder = async (
|
|
366
|
+
billingAddress: Record<string, string>,
|
|
367
|
+
shippingAddress: Record<string, string>,
|
|
368
|
+
gatewayId: string
|
|
369
|
+
) => {
|
|
370
|
+
setIsSubmitting(true);
|
|
371
|
+
setError(null);
|
|
372
|
+
|
|
373
|
+
if (!IS_DEV) {
|
|
374
|
+
try {
|
|
375
|
+
// 1. Update customer billing/shipping details in session
|
|
376
|
+
await fetchStoreApi("cart/update-customer", {
|
|
377
|
+
method: "POST",
|
|
378
|
+
body: JSON.stringify({
|
|
379
|
+
billing_address: billingAddress,
|
|
380
|
+
shipping_address: shippingAddress
|
|
381
|
+
})
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// 2. Submit order to checkout
|
|
385
|
+
const checkoutResult = await fetchStoreApi("checkout", {
|
|
386
|
+
method: "POST",
|
|
387
|
+
body: JSON.stringify({
|
|
388
|
+
billing_address: billingAddress,
|
|
389
|
+
shipping_address: shippingAddress,
|
|
390
|
+
payment_method: gatewayId,
|
|
391
|
+
payment_data: []
|
|
392
|
+
})
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
cartContext.clearCart();
|
|
396
|
+
setIsSubmitting(false);
|
|
397
|
+
|
|
398
|
+
const status = checkoutResult.payment_result?.payment_status;
|
|
399
|
+
const redirectUrl = checkoutResult.payment_result?.redirect_url || "/checkout/order-received";
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
success: status === "success" || status === "pending" || !!checkoutResult.order_id,
|
|
403
|
+
orderId: checkoutResult.order_id,
|
|
404
|
+
redirectUrl: redirectUrl
|
|
405
|
+
};
|
|
406
|
+
} catch (err: any) {
|
|
407
|
+
setError(err.message || "Failed to process order. Please try again.");
|
|
408
|
+
setIsSubmitting(false);
|
|
409
|
+
return { success: false };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Simulate network submission delay in dev mode
|
|
414
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
415
|
+
|
|
416
|
+
if (!billingAddress.first_name || !billingAddress.email || !billingAddress.address_1) {
|
|
417
|
+
setError("Please complete all required billing fields.");
|
|
418
|
+
setIsSubmitting(false);
|
|
419
|
+
return { success: false };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Verify shippingAddress and gatewayId parameters to satisfy compiler
|
|
423
|
+
if (!shippingAddress || !gatewayId) {
|
|
424
|
+
setError("Shipping details and payment method are required.");
|
|
425
|
+
setIsSubmitting(false);
|
|
426
|
+
return { success: false };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Success response simulation
|
|
430
|
+
cartContext.clearCart();
|
|
431
|
+
setIsSubmitting(false);
|
|
432
|
+
return {
|
|
433
|
+
success: true,
|
|
434
|
+
orderId: Math.floor(100000 + Math.random() * 90000),
|
|
435
|
+
redirectUrl: "/checkout/order-received"
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
shippingMethods,
|
|
441
|
+
paymentGateways,
|
|
442
|
+
processOrder,
|
|
443
|
+
isSubmitting,
|
|
444
|
+
error
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── 3. Customer & Accounts Hook ─────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
export interface CustomerProfile {
|
|
451
|
+
username: string;
|
|
452
|
+
email: string;
|
|
453
|
+
first_name: string;
|
|
454
|
+
last_name: string;
|
|
455
|
+
billing: Record<string, string>;
|
|
456
|
+
shipping: Record<string, string>;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface OrderRecord {
|
|
460
|
+
id: number;
|
|
461
|
+
date: string;
|
|
462
|
+
status: string;
|
|
463
|
+
total: string;
|
|
464
|
+
itemsCount: number;
|
|
465
|
+
items: Array<{ title: string; qty: number }>;
|
|
466
|
+
downloadableFiles?: Array<{ name: string; url: string }>;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function useWpCustomer() {
|
|
470
|
+
const [isLoggedIn, setIsLoggedIn] = React.useState(false);
|
|
471
|
+
const [customer, setCustomer] = React.useState<CustomerProfile | null>(null);
|
|
472
|
+
const [orders, setOrders] = React.useState<OrderRecord[]>([]);
|
|
473
|
+
const [loading, setLoading] = React.useState(false);
|
|
474
|
+
|
|
475
|
+
// Initialize from LocalStorage in browser
|
|
476
|
+
React.useEffect(() => {
|
|
477
|
+
if (typeof window !== "undefined") {
|
|
478
|
+
const storedCust = localStorage.getItem("forgewp-customer");
|
|
479
|
+
const storedOrders = localStorage.getItem("forgewp-customer-orders");
|
|
480
|
+
if (storedCust) {
|
|
481
|
+
setCustomer(JSON.parse(storedCust));
|
|
482
|
+
setIsLoggedIn(true);
|
|
483
|
+
}
|
|
484
|
+
if (storedOrders) {
|
|
485
|
+
setOrders(JSON.parse(storedOrders));
|
|
486
|
+
} else {
|
|
487
|
+
// Seed default order history if empty
|
|
488
|
+
const defaultOrders: OrderRecord[] = [
|
|
489
|
+
{
|
|
490
|
+
id: 9812,
|
|
491
|
+
date: "June 1, 2026",
|
|
492
|
+
status: "completed",
|
|
493
|
+
total: "$59.99",
|
|
494
|
+
itemsCount: 1,
|
|
495
|
+
items: [{ title: "Minimalist Brutalist Hoodie", qty: 1 }]
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: 9410,
|
|
499
|
+
date: "May 15, 2026",
|
|
500
|
+
status: "completed",
|
|
501
|
+
total: "$9.99",
|
|
502
|
+
itemsCount: 1,
|
|
503
|
+
items: [{ title: "Brutalist Poster Art Print", qty: 1 }],
|
|
504
|
+
downloadableFiles: [{ name: "High-Res PDF Vector", url: "https://example.com/downloads/brutalist-poster.pdf" }]
|
|
505
|
+
}
|
|
506
|
+
];
|
|
507
|
+
setOrders(defaultOrders);
|
|
508
|
+
localStorage.setItem("forgewp-customer-orders", JSON.stringify(defaultOrders));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}, []);
|
|
512
|
+
|
|
513
|
+
const login = async (userEmail: string, pass: string) => {
|
|
514
|
+
setLoading(true);
|
|
515
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
516
|
+
|
|
517
|
+
if (!userEmail || !pass) {
|
|
518
|
+
setLoading(false);
|
|
519
|
+
throw new Error("Username and Password are required.");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const mockProfile: CustomerProfile = {
|
|
523
|
+
username: userEmail.split("@")[0],
|
|
524
|
+
email: userEmail,
|
|
525
|
+
first_name: "John",
|
|
526
|
+
last_name: "Doe",
|
|
527
|
+
billing: {
|
|
528
|
+
first_name: "John",
|
|
529
|
+
last_name: "Doe",
|
|
530
|
+
email: userEmail,
|
|
531
|
+
address_1: "128 Concrete Alley",
|
|
532
|
+
city: "Berlin",
|
|
533
|
+
postcode: "10115",
|
|
534
|
+
country: "DE"
|
|
535
|
+
},
|
|
536
|
+
shipping: {
|
|
537
|
+
first_name: "John",
|
|
538
|
+
last_name: "Doe",
|
|
539
|
+
address_1: "128 Concrete Alley",
|
|
540
|
+
city: "Berlin",
|
|
541
|
+
postcode: "10115",
|
|
542
|
+
country: "DE"
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
setCustomer(mockProfile);
|
|
547
|
+
setIsLoggedIn(true);
|
|
548
|
+
localStorage.setItem("forgewp-customer", JSON.stringify(mockProfile));
|
|
549
|
+
setLoading(false);
|
|
550
|
+
return true;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const register = async (username: string, email: string, pass: string) => {
|
|
554
|
+
setLoading(true);
|
|
555
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
556
|
+
|
|
557
|
+
if (!username || !email || !pass) {
|
|
558
|
+
setLoading(false);
|
|
559
|
+
throw new Error("All registration fields are required.");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const mockProfile: CustomerProfile = {
|
|
563
|
+
username,
|
|
564
|
+
email,
|
|
565
|
+
first_name: username,
|
|
566
|
+
last_name: "",
|
|
567
|
+
billing: { first_name: username, email, country: "US" },
|
|
568
|
+
shipping: { first_name: username, country: "US" }
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
setCustomer(mockProfile);
|
|
572
|
+
setIsLoggedIn(true);
|
|
573
|
+
localStorage.setItem("forgewp-customer", JSON.stringify(mockProfile));
|
|
574
|
+
setLoading(false);
|
|
575
|
+
return true;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const logout = () => {
|
|
579
|
+
setCustomer(null);
|
|
580
|
+
setIsLoggedIn(false);
|
|
581
|
+
localStorage.removeItem("forgewp-customer");
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
isLoggedIn,
|
|
586
|
+
customer,
|
|
587
|
+
orders,
|
|
588
|
+
login,
|
|
589
|
+
register,
|
|
590
|
+
logout,
|
|
591
|
+
loading
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ── 4. Faceted Search & Filters Hook ────────────────────────────────────────
|
|
596
|
+
|
|
597
|
+
export interface ActiveFiltersState {
|
|
598
|
+
categories: string[];
|
|
599
|
+
tags: string[];
|
|
600
|
+
attributes: Record<string, string[]>;
|
|
601
|
+
priceRange: [number, number];
|
|
602
|
+
sortBy: string;
|
|
603
|
+
search: string;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function useWpProductFilters() {
|
|
607
|
+
const [activeFilters, setActiveFilters] = React.useState<ActiveFiltersState>({
|
|
608
|
+
categories: [],
|
|
609
|
+
tags: [],
|
|
610
|
+
attributes: {},
|
|
611
|
+
priceRange: [0, 100],
|
|
612
|
+
sortBy: "date",
|
|
613
|
+
search: ""
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Load URL Search Parameters on mount to mimic production behavior
|
|
617
|
+
React.useEffect(() => {
|
|
618
|
+
if (typeof window !== "undefined") {
|
|
619
|
+
const params = new URLSearchParams(window.location.search);
|
|
620
|
+
const categories: string[] = [];
|
|
621
|
+
const tags: string[] = [];
|
|
622
|
+
const attributes: Record<string, string[]> = {};
|
|
623
|
+
let priceRange: [number, number] = [0, 100];
|
|
624
|
+
let sortBy = "date";
|
|
625
|
+
let search = "";
|
|
626
|
+
|
|
627
|
+
params.forEach((value, key) => {
|
|
628
|
+
if (key === "product_cat") {
|
|
629
|
+
categories.push(value);
|
|
630
|
+
} else if (key === "product_tag") {
|
|
631
|
+
tags.push(value);
|
|
632
|
+
} else if (key === "orderby") {
|
|
633
|
+
sortBy = value;
|
|
634
|
+
} else if (key === "s" || key === "q") {
|
|
635
|
+
search = value;
|
|
636
|
+
} else if (key === "min_price") {
|
|
637
|
+
priceRange[0] = parseFloat(value);
|
|
638
|
+
} else if (key === "max_price") {
|
|
639
|
+
priceRange[1] = parseFloat(value);
|
|
640
|
+
} else if (key.startsWith("filter_")) {
|
|
641
|
+
const attrName = key.replace("filter_", "");
|
|
642
|
+
attributes[attrName] = value.split(",");
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
setActiveFilters({
|
|
647
|
+
categories,
|
|
648
|
+
tags,
|
|
649
|
+
attributes,
|
|
650
|
+
priceRange,
|
|
651
|
+
sortBy,
|
|
652
|
+
search
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}, []);
|
|
656
|
+
|
|
657
|
+
// Update URL parameters
|
|
658
|
+
const updateUrl = (filters: ActiveFiltersState) => {
|
|
659
|
+
if (typeof window !== "undefined") {
|
|
660
|
+
const params = new URLSearchParams();
|
|
661
|
+
|
|
662
|
+
filters.categories.forEach(c => params.append("product_cat", c));
|
|
663
|
+
filters.tags.forEach(t => params.append("product_tag", t));
|
|
664
|
+
|
|
665
|
+
Object.entries(filters.attributes).forEach(([name, vals]) => {
|
|
666
|
+
if (vals.length > 0) {
|
|
667
|
+
params.append(`filter_${name}`, vals.join(","));
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
if (filters.priceRange[0] > 0) {
|
|
672
|
+
params.append("min_price", String(filters.priceRange[0]));
|
|
673
|
+
}
|
|
674
|
+
if (filters.priceRange[1] < 100) {
|
|
675
|
+
params.append("max_price", String(filters.priceRange[1]));
|
|
676
|
+
}
|
|
677
|
+
if (filters.sortBy !== "date") {
|
|
678
|
+
params.append("orderby", filters.sortBy);
|
|
679
|
+
}
|
|
680
|
+
if (filters.search) {
|
|
681
|
+
params.append("s", filters.search);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const newSearch = params.toString();
|
|
685
|
+
const newUrl = `${window.location.pathname}${newSearch ? "?" + newSearch : ""}`;
|
|
686
|
+
window.history.pushState(null, "", newUrl);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const setFilter = (type: "category" | "tag" | string, value: string, active: boolean) => {
|
|
691
|
+
setActiveFilters((prev) => {
|
|
692
|
+
let updated: ActiveFiltersState;
|
|
693
|
+
if (type === "category") {
|
|
694
|
+
const categories = active
|
|
695
|
+
? [...prev.categories, value]
|
|
696
|
+
: prev.categories.filter(c => c !== value);
|
|
697
|
+
updated = { ...prev, categories };
|
|
698
|
+
} else if (type === "tag") {
|
|
699
|
+
const tags = active
|
|
700
|
+
? [...prev.tags, value]
|
|
701
|
+
: prev.tags.filter(t => t !== value);
|
|
702
|
+
updated = { ...prev, tags };
|
|
703
|
+
} else {
|
|
704
|
+
// Attribute filter
|
|
705
|
+
const currentVals = prev.attributes[type] || [];
|
|
706
|
+
const updatedVals = active
|
|
707
|
+
? [...currentVals, value]
|
|
708
|
+
: currentVals.filter(v => v !== value);
|
|
709
|
+
|
|
710
|
+
const attributes = { ...prev.attributes, [type]: updatedVals };
|
|
711
|
+
if (updatedVals.length === 0) {
|
|
712
|
+
delete attributes[type];
|
|
713
|
+
}
|
|
714
|
+
updated = { ...prev, attributes };
|
|
715
|
+
}
|
|
716
|
+
updateUrl(updated);
|
|
717
|
+
return updated;
|
|
718
|
+
});
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const setPriceRange = (min: number, max: number) => {
|
|
722
|
+
setActiveFilters((prev) => {
|
|
723
|
+
const updated = { ...prev, priceRange: [min, max] as [number, number] };
|
|
724
|
+
updateUrl(updated);
|
|
725
|
+
return updated;
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const setSortBy = (sortOption: string) => {
|
|
730
|
+
setActiveFilters((prev) => {
|
|
731
|
+
const updated = { ...prev, sortBy: sortOption };
|
|
732
|
+
updateUrl(updated);
|
|
733
|
+
return updated;
|
|
734
|
+
});
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const resetFilters = () => {
|
|
738
|
+
const cleared: ActiveFiltersState = {
|
|
739
|
+
categories: [],
|
|
740
|
+
tags: [],
|
|
741
|
+
attributes: {},
|
|
742
|
+
priceRange: [0, 100],
|
|
743
|
+
sortBy: "date",
|
|
744
|
+
search: ""
|
|
745
|
+
};
|
|
746
|
+
setActiveFilters(cleared);
|
|
747
|
+
updateUrl(cleared);
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
activeFilters,
|
|
752
|
+
setFilter,
|
|
753
|
+
setPriceRange,
|
|
754
|
+
setSortBy,
|
|
755
|
+
resetFilters
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── 5. Wishlist / Favorites Hook ────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
export function useWpWishlist() {
|
|
762
|
+
const [wishlist, setWishlist] = React.useState<number[]>([]);
|
|
763
|
+
|
|
764
|
+
React.useEffect(() => {
|
|
765
|
+
if (typeof window !== "undefined") {
|
|
766
|
+
const stored = localStorage.getItem("forgewp-wishlist");
|
|
767
|
+
if (stored) {
|
|
768
|
+
setWishlist(JSON.parse(stored));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}, []);
|
|
772
|
+
|
|
773
|
+
const isWishlisted = React.useCallback((id: number) => {
|
|
774
|
+
return wishlist.includes(id);
|
|
775
|
+
}, [wishlist]);
|
|
776
|
+
|
|
777
|
+
const toggleWishlist = (id: number) => {
|
|
778
|
+
setWishlist((prev) => {
|
|
779
|
+
const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id];
|
|
780
|
+
if (typeof window !== "undefined") {
|
|
781
|
+
localStorage.setItem("forgewp-wishlist", JSON.stringify(next));
|
|
782
|
+
}
|
|
783
|
+
return next;
|
|
784
|
+
});
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
return {
|
|
788
|
+
wishlist,
|
|
789
|
+
isWishlisted,
|
|
790
|
+
toggleWishlist
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export function useWpProductPrice(): string {
|
|
795
|
+
if (!IS_DEV) {
|
|
796
|
+
return '__FORGEWP_PRODUCT_PRICE__';
|
|
797
|
+
}
|
|
798
|
+
const post = React.useContext(WpPostContext);
|
|
799
|
+
if (!post || post.__postType !== "product") return "$19.99";
|
|
800
|
+
const reg = post.customFields?.regular_price;
|
|
801
|
+
const price = post.customFields?.price;
|
|
802
|
+
if (post.customFields?.on_sale && reg && reg !== price) {
|
|
803
|
+
return `$${price} (Sale: was $${reg})`;
|
|
804
|
+
}
|
|
805
|
+
return `$${price}`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function useWpProductSKU(): string {
|
|
809
|
+
if (!IS_DEV) {
|
|
810
|
+
return '__FORGEWP_PRODUCT_SKU__';
|
|
811
|
+
}
|
|
812
|
+
const post = React.useContext(WpPostContext);
|
|
813
|
+
if (!post || post.__postType !== "product") return "SKU-MOCK";
|
|
814
|
+
return post.customFields?.sku || "";
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function useWpProductRating(): string {
|
|
818
|
+
if (!IS_DEV) {
|
|
819
|
+
return '__FORGEWP_PRODUCT_RATING__';
|
|
820
|
+
}
|
|
821
|
+
const post = React.useContext(WpPostContext);
|
|
822
|
+
if (!post || post.__postType !== "product") return "4.5";
|
|
823
|
+
return post.customFields?.average_rating || "5.0";
|
|
824
|
+
}
|
|
825
|
+
|