@forgecart/cli 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +2 -1
- package/dist/src/commands/delete.js +2 -0
- package/dist/src/commands/delete.js.map +1 -1
- package/dist/src/commands/deploy.d.ts.map +1 -1
- package/dist/src/commands/deploy.js +36 -28
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/env.js +15 -8
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/init.js +56 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/list.js +2 -0
- package/dist/src/commands/list.js.map +1 -1
- package/dist/src/commands/scale.js +1 -0
- package/dist/src/commands/scale.js.map +1 -1
- package/dist/src/commands/status.js +3 -0
- package/dist/src/commands/status.js.map +1 -1
- package/dist/src/commands/whoami.js +2 -0
- package/dist/src/commands/whoami.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +10 -4
- package/dist/src/lib/api-client.d.ts.map +1 -1
- package/dist/src/lib/api-client.js +11 -7
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/env-sync.d.ts +13 -5
- package/dist/src/lib/env-sync.d.ts.map +1 -1
- package/dist/src/lib/env-sync.js +120 -31
- package/dist/src/lib/env-sync.js.map +1 -1
- package/dist/src/lib/nx-generator.d.ts +5 -3
- package/dist/src/lib/nx-generator.d.ts.map +1 -1
- package/dist/src/lib/nx-generator.js +37 -30
- package/dist/src/lib/nx-generator.js.map +1 -1
- package/dist/src/utils/spinner.d.ts +12 -0
- package/dist/src/utils/spinner.d.ts.map +1 -1
- package/dist/src/utils/spinner.js +68 -0
- package/dist/src/utils/spinner.js.map +1 -1
- package/dist/templates/application-additions/src/app/app.module.ts +6 -6
- package/dist/templates/application-additions/src/services/index.ts +1 -1
- package/dist/templates/application-additions/src/services/product-event-listener.service.ts +150 -0
- package/dist/templates/application-e2e-additions/src/support/global-setup.ts +16 -0
- package/dist/templates/application-e2e-additions/src/support/test-setup.ts +9 -0
- package/dist/templates/storefront-additions/postcss.config.js +5 -0
- package/dist/templates/storefront-additions/src/app/cart/page.tsx +77 -0
- package/dist/templates/storefront-additions/src/app/global.css +1 -0
- package/dist/templates/storefront-additions/src/app/layout.tsx +25 -0
- package/dist/templates/storefront-additions/src/app/page.tsx +43 -0
- package/dist/templates/storefront-additions/src/components/AddToCartButton.tsx +29 -0
- package/dist/templates/storefront-additions/src/components/CartItem.tsx +82 -0
- package/dist/templates/storefront-additions/src/components/Header.tsx +42 -0
- package/dist/templates/storefront-additions/src/components/ProductCard.tsx +57 -0
- package/dist/templates/storefront-additions/src/components/QuantitySelector.tsx +35 -0
- package/dist/templates/storefront-additions/src/context/CartContext.tsx +158 -0
- package/dist/templates/storefront-additions/tailwind.config.js +8 -0
- package/dist/templates/storefront-e2e-additions/playwright.config.ts +26 -0
- package/dist/templates/storefront-e2e-additions/src/example.spec.ts +11 -0
- package/package.json +2 -2
- package/templates/application-additions/src/app/app.module.ts +6 -6
- package/templates/application-additions/src/services/index.ts +1 -1
- package/templates/application-additions/src/services/product-event-listener.service.ts +150 -0
- package/templates/application-e2e-additions/src/support/global-setup.ts +16 -0
- package/templates/application-e2e-additions/src/support/test-setup.ts +9 -0
- package/templates/storefront-additions/postcss.config.js +5 -0
- package/templates/storefront-additions/src/app/cart/page.tsx +77 -0
- package/templates/storefront-additions/src/app/global.css +1 -0
- package/templates/storefront-additions/src/app/layout.tsx +25 -0
- package/templates/storefront-additions/src/app/page.tsx +43 -0
- package/templates/storefront-additions/src/components/AddToCartButton.tsx +29 -0
- package/templates/storefront-additions/src/components/CartItem.tsx +82 -0
- package/templates/storefront-additions/src/components/Header.tsx +42 -0
- package/templates/storefront-additions/src/components/ProductCard.tsx +57 -0
- package/templates/storefront-additions/src/components/QuantitySelector.tsx +35 -0
- package/templates/storefront-additions/src/context/CartContext.tsx +158 -0
- package/templates/storefront-additions/tailwind.config.js +8 -0
- package/templates/storefront-e2e-additions/playwright.config.ts +26 -0
- package/templates/storefront-e2e-additions/src/example.spec.ts +11 -0
- package/dist/templates/application-additions/env.example +0 -7
- package/dist/templates/application-additions/src/services/price-fetcher.service.ts +0 -71
- package/dist/templates/storefront-additions/env.example +0 -3
- package/dist/templates/storefront-additions/src/app/products/page.tsx +0 -66
- package/templates/application-additions/env.example +0 -7
- package/templates/application-additions/src/services/price-fetcher.service.ts +0 -71
- package/templates/storefront-additions/env.example +0 -3
- package/templates/storefront-additions/src/app/products/page.tsx +0 -66
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCart } from '../context/CartContext'
|
|
4
|
+
import { QuantitySelector } from './QuantitySelector'
|
|
5
|
+
|
|
6
|
+
interface CartLine {
|
|
7
|
+
id: string
|
|
8
|
+
quantity: number
|
|
9
|
+
productVariant: {
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
price: number
|
|
13
|
+
priceWithTax: number
|
|
14
|
+
currencyCode: string
|
|
15
|
+
}
|
|
16
|
+
featuredAsset?: {
|
|
17
|
+
preview: string
|
|
18
|
+
}
|
|
19
|
+
unitPriceWithTax: number
|
|
20
|
+
linePriceWithTax: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CartItemProps {
|
|
24
|
+
line: CartLine
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CartItem({ line }: CartItemProps) {
|
|
28
|
+
const { updateQuantity, removeItem, loading } = useCart()
|
|
29
|
+
|
|
30
|
+
// Defensive check for incomplete line data
|
|
31
|
+
if (!line.productVariant) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const unitPrice = (line.unitPriceWithTax / 100).toFixed(2)
|
|
36
|
+
const lineTotal = (line.linePriceWithTax / 100).toFixed(2)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex gap-4 py-4 border-b">
|
|
40
|
+
{line.featuredAsset && (
|
|
41
|
+
<img
|
|
42
|
+
src={line.featuredAsset.preview}
|
|
43
|
+
alt={line.productVariant.name}
|
|
44
|
+
className="w-20 h-20 object-cover rounded"
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
{!line.featuredAsset && (
|
|
48
|
+
<div className="w-20 h-20 bg-gray-200 rounded flex items-center justify-center">
|
|
49
|
+
<span className="text-gray-400 text-xs">No img</span>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<div className="flex-1">
|
|
53
|
+
<h3 className="font-semibold text-gray-900">
|
|
54
|
+
{line.productVariant.name}
|
|
55
|
+
</h3>
|
|
56
|
+
<p className="text-gray-600 text-sm">
|
|
57
|
+
{unitPrice} {line.productVariant.currencyCode} each
|
|
58
|
+
</p>
|
|
59
|
+
<div className="flex items-center justify-between mt-2">
|
|
60
|
+
<QuantitySelector
|
|
61
|
+
quantity={line.quantity}
|
|
62
|
+
onIncrease={() => updateQuantity(line.id, line.quantity + 1)}
|
|
63
|
+
onDecrease={() => updateQuantity(line.id, line.quantity - 1)}
|
|
64
|
+
disabled={loading}
|
|
65
|
+
/>
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => removeItem(line.id)}
|
|
68
|
+
disabled={loading}
|
|
69
|
+
className="text-red-600 hover:text-red-800 text-sm disabled:opacity-50"
|
|
70
|
+
>
|
|
71
|
+
Remove
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="text-right">
|
|
76
|
+
<p className="font-bold text-gray-900">
|
|
77
|
+
{lineTotal} {line.productVariant.currencyCode}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useCart } from '../context/CartContext'
|
|
5
|
+
|
|
6
|
+
export function Header() {
|
|
7
|
+
const { itemCount, loading } = useCart()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<header className="bg-white shadow-sm border-b">
|
|
11
|
+
<div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
|
|
12
|
+
<Link href="/" className="text-xl font-bold text-gray-900">
|
|
13
|
+
Store
|
|
14
|
+
</Link>
|
|
15
|
+
<Link
|
|
16
|
+
href="/cart"
|
|
17
|
+
className="relative flex items-center gap-2 text-gray-700 hover:text-gray-900"
|
|
18
|
+
>
|
|
19
|
+
<svg
|
|
20
|
+
className="w-6 h-6"
|
|
21
|
+
fill="none"
|
|
22
|
+
stroke="currentColor"
|
|
23
|
+
viewBox="0 0 24 24"
|
|
24
|
+
>
|
|
25
|
+
<path
|
|
26
|
+
strokeLinecap="round"
|
|
27
|
+
strokeLinejoin="round"
|
|
28
|
+
strokeWidth={2}
|
|
29
|
+
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
|
|
30
|
+
/>
|
|
31
|
+
</svg>
|
|
32
|
+
<span>Cart</span>
|
|
33
|
+
{itemCount > 0 && (
|
|
34
|
+
<span className="absolute -top-2 -right-2 bg-blue-600 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
|
|
35
|
+
{loading ? '...' : itemCount}
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
</Link>
|
|
39
|
+
</div>
|
|
40
|
+
</header>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { AddToCartButton } from './AddToCartButton'
|
|
2
|
+
|
|
3
|
+
interface ProductVariant {
|
|
4
|
+
id: string
|
|
5
|
+
price: number
|
|
6
|
+
priceWithTax: number
|
|
7
|
+
currencyCode: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Product {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
featuredAsset?: {
|
|
15
|
+
preview: string
|
|
16
|
+
}
|
|
17
|
+
variants: ProductVariant[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ProductCardProps {
|
|
21
|
+
product: Product
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ProductCard({ product }: ProductCardProps) {
|
|
25
|
+
const variant = product.variants[0]
|
|
26
|
+
const price = variant ? (variant.priceWithTax / 100).toFixed(2) : '0.00'
|
|
27
|
+
const currencyCode = variant?.currencyCode ?? 'USD'
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="bg-white border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
|
31
|
+
{product.featuredAsset && (
|
|
32
|
+
<img
|
|
33
|
+
src={product.featuredAsset.preview}
|
|
34
|
+
alt={product.name}
|
|
35
|
+
className="w-full h-48 object-cover"
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
{!product.featuredAsset && (
|
|
39
|
+
<div className="w-full h-48 bg-gray-200 flex items-center justify-center">
|
|
40
|
+
<span className="text-gray-400">No image</span>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<div className="p-4">
|
|
44
|
+
<h2 className="text-lg font-semibold mb-2 text-gray-900">
|
|
45
|
+
{product.name}
|
|
46
|
+
</h2>
|
|
47
|
+
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
|
|
48
|
+
{product.description}
|
|
49
|
+
</p>
|
|
50
|
+
<p className="text-lg font-bold text-blue-600 mb-4">
|
|
51
|
+
{price} {currencyCode}
|
|
52
|
+
</p>
|
|
53
|
+
{variant && <AddToCartButton productVariantId={variant.id} />}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
interface QuantitySelectorProps {
|
|
4
|
+
quantity: number
|
|
5
|
+
onIncrease: () => void
|
|
6
|
+
onDecrease: () => void
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function QuantitySelector({
|
|
11
|
+
quantity,
|
|
12
|
+
onIncrease,
|
|
13
|
+
onDecrease,
|
|
14
|
+
disabled,
|
|
15
|
+
}: QuantitySelectorProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex items-center gap-2">
|
|
18
|
+
<button
|
|
19
|
+
onClick={onDecrease}
|
|
20
|
+
disabled={disabled || quantity <= 1}
|
|
21
|
+
className="w-8 h-8 flex items-center justify-center border rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
22
|
+
>
|
|
23
|
+
-
|
|
24
|
+
</button>
|
|
25
|
+
<span className="w-8 text-center">{quantity}</span>
|
|
26
|
+
<button
|
|
27
|
+
onClick={onIncrease}
|
|
28
|
+
disabled={disabled}
|
|
29
|
+
className="w-8 h-8 flex items-center justify-center border rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
30
|
+
>
|
|
31
|
+
+
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useEffect,
|
|
8
|
+
useCallback,
|
|
9
|
+
ReactNode,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import forgecart from '../lib/forgecart'
|
|
12
|
+
|
|
13
|
+
interface CartLine {
|
|
14
|
+
id: string
|
|
15
|
+
quantity: number
|
|
16
|
+
productVariant: {
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
price: number
|
|
20
|
+
priceWithTax: number
|
|
21
|
+
currencyCode: string
|
|
22
|
+
}
|
|
23
|
+
featuredAsset?: {
|
|
24
|
+
preview: string
|
|
25
|
+
}
|
|
26
|
+
unitPriceWithTax: number
|
|
27
|
+
linePriceWithTax: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Cart {
|
|
31
|
+
id: string
|
|
32
|
+
lines: CartLine[]
|
|
33
|
+
totalQuantity: number
|
|
34
|
+
subTotalWithTax: number
|
|
35
|
+
totalWithTax: number
|
|
36
|
+
currencyCode: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CartContextType {
|
|
40
|
+
cart: Cart | null
|
|
41
|
+
loading: boolean
|
|
42
|
+
itemCount: number
|
|
43
|
+
addToCart: (productVariantId: string, quantity: number) => Promise<void>
|
|
44
|
+
updateQuantity: (orderLineId: string, quantity: number) => Promise<void>
|
|
45
|
+
removeItem: (orderLineId: string) => Promise<void>
|
|
46
|
+
refreshCart: () => Promise<void>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CartContext = createContext<CartContextType | undefined>(undefined)
|
|
50
|
+
|
|
51
|
+
export function CartProvider({ children }: { children: ReactNode }) {
|
|
52
|
+
const [cart, setCart] = useState<Cart | null>(null)
|
|
53
|
+
const [loading, setLoading] = useState(true)
|
|
54
|
+
|
|
55
|
+
const refreshCart = useCallback(async () => {
|
|
56
|
+
try {
|
|
57
|
+
const result = await forgecart.shop.order.activeOrder()
|
|
58
|
+
if (result.activeOrder) {
|
|
59
|
+
setCart(result.activeOrder as unknown as Cart)
|
|
60
|
+
} else {
|
|
61
|
+
setCart(null)
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to fetch cart:', error)
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false)
|
|
67
|
+
}
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
refreshCart()
|
|
72
|
+
}, [refreshCart])
|
|
73
|
+
|
|
74
|
+
const addToCart = async (productVariantId: string, quantity: number) => {
|
|
75
|
+
setLoading(true)
|
|
76
|
+
try {
|
|
77
|
+
const result = await forgecart.shop.order.addItemToOrder({
|
|
78
|
+
productVariantId,
|
|
79
|
+
quantity,
|
|
80
|
+
})
|
|
81
|
+
if (
|
|
82
|
+
result.addItemToOrder &&
|
|
83
|
+
result.addItemToOrder.__typename === 'Order'
|
|
84
|
+
) {
|
|
85
|
+
setCart(result.addItemToOrder as unknown as Cart)
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Failed to add item:', error)
|
|
89
|
+
} finally {
|
|
90
|
+
setLoading(false)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const updateQuantity = async (orderLineId: string, quantity: number) => {
|
|
95
|
+
setLoading(true)
|
|
96
|
+
try {
|
|
97
|
+
const result = await forgecart.shop.order.adjustOrderLine({
|
|
98
|
+
orderLineId,
|
|
99
|
+
quantity,
|
|
100
|
+
})
|
|
101
|
+
if (
|
|
102
|
+
result.adjustOrderLine &&
|
|
103
|
+
result.adjustOrderLine.__typename === 'Order'
|
|
104
|
+
) {
|
|
105
|
+
setCart(result.adjustOrderLine as unknown as Cart)
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Failed to update quantity:', error)
|
|
109
|
+
} finally {
|
|
110
|
+
setLoading(false)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const removeItem = async (orderLineId: string) => {
|
|
115
|
+
setLoading(true)
|
|
116
|
+
try {
|
|
117
|
+
const result = await forgecart.shop.order.removeOrderLine({
|
|
118
|
+
orderLineId,
|
|
119
|
+
})
|
|
120
|
+
if (
|
|
121
|
+
result.removeOrderLine &&
|
|
122
|
+
result.removeOrderLine.__typename === 'Order'
|
|
123
|
+
) {
|
|
124
|
+
setCart(result.removeOrderLine as unknown as Cart)
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Failed to remove item:', error)
|
|
128
|
+
} finally {
|
|
129
|
+
setLoading(false)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const itemCount = cart?.totalQuantity ?? 0
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<CartContext.Provider
|
|
137
|
+
value={{
|
|
138
|
+
cart,
|
|
139
|
+
loading,
|
|
140
|
+
itemCount,
|
|
141
|
+
addToCart,
|
|
142
|
+
updateQuantity,
|
|
143
|
+
removeItem,
|
|
144
|
+
refreshCart,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</CartContext.Provider>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function useCart() {
|
|
153
|
+
const context = useContext(CartContext)
|
|
154
|
+
if (!context) {
|
|
155
|
+
throw new Error('useCart must be used within CartProvider')
|
|
156
|
+
}
|
|
157
|
+
return context
|
|
158
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
import { nxE2EPreset } from '@nx/playwright/preset'
|
|
3
|
+
|
|
4
|
+
import { workspaceRoot } from '@nx/devkit'
|
|
5
|
+
|
|
6
|
+
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
...nxE2EPreset(__filename, { testDir: './src' }),
|
|
10
|
+
use: {
|
|
11
|
+
baseURL,
|
|
12
|
+
trace: 'on-first-retry',
|
|
13
|
+
},
|
|
14
|
+
webServer: {
|
|
15
|
+
command: 'npx nx run storefront:dev',
|
|
16
|
+
url: 'http://localhost:4200',
|
|
17
|
+
reuseExistingServer: !process.env.CI,
|
|
18
|
+
cwd: workspaceRoot,
|
|
19
|
+
},
|
|
20
|
+
projects: [
|
|
21
|
+
{
|
|
22
|
+
name: 'chromium',
|
|
23
|
+
use: { ...devices['Desktop Chrome'] },
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
test('has title', async ({ page }) => {
|
|
4
|
+
await page.goto('/')
|
|
5
|
+
expect(await page.locator('h1').innerText()).toContain('Our Products')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('has cart link', async ({ page }) => {
|
|
9
|
+
await page.goto('/')
|
|
10
|
+
await expect(page.locator('a[href="/cart"]')).toBeVisible()
|
|
11
|
+
})
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common'
|
|
2
|
-
import { ForgeCartService } from './forgecart.service'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Example service demonstrating ForgeCart SDK integration
|
|
6
|
-
*
|
|
7
|
-
* This can be used for custom backend tasks like:
|
|
8
|
-
* - Scheduled price updates
|
|
9
|
-
* - Inventory synchronization
|
|
10
|
-
* - Custom order processing
|
|
11
|
-
* - Third-party integrations
|
|
12
|
-
*/
|
|
13
|
-
@Injectable()
|
|
14
|
-
export class PriceFetcherService {
|
|
15
|
-
constructor(private readonly forgeCart: ForgeCartService) {}
|
|
16
|
-
|
|
17
|
-
async getProductPrices(productIds: string[]): Promise<
|
|
18
|
-
Array<{
|
|
19
|
-
productId: string
|
|
20
|
-
name: string
|
|
21
|
-
price: number
|
|
22
|
-
}>
|
|
23
|
-
> {
|
|
24
|
-
const prices = []
|
|
25
|
-
|
|
26
|
-
for (const productId of productIds) {
|
|
27
|
-
try {
|
|
28
|
-
const result = await this.forgeCart.shop.products.product({
|
|
29
|
-
id: productId,
|
|
30
|
-
})
|
|
31
|
-
const product = result.product
|
|
32
|
-
if (product && product.variants?.[0]) {
|
|
33
|
-
prices.push({
|
|
34
|
-
productId,
|
|
35
|
-
name: product.name,
|
|
36
|
-
price: product.variants[0].price,
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error(`Failed to fetch price for product ${productId}:`, error)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return prices
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async syncInventory(): Promise<{ synced: number; errors: number }> {
|
|
48
|
-
// Example: Sync inventory with external system
|
|
49
|
-
// This is a placeholder for custom business logic
|
|
50
|
-
let synced = 0
|
|
51
|
-
let errors = 0
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const result = await this.forgeCart.shop.products.products({
|
|
55
|
-
options: { take: 100 },
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
for (const product of result.products.items || []) {
|
|
59
|
-
// Here you would implement your custom sync logic
|
|
60
|
-
// e.g., update external ERP, sync with warehouse system, etc.
|
|
61
|
-
console.log(`Syncing product: ${product.name}`)
|
|
62
|
-
synced++
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error('Inventory sync failed:', error)
|
|
66
|
-
errors++
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return { synced, errors }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { ProductList, Product } from '@forgecart/sdk/shop'
|
|
2
|
-
import forgecart from '../../lib/forgecart'
|
|
3
|
-
|
|
4
|
-
async function getProducts(): Promise<ProductList> {
|
|
5
|
-
try {
|
|
6
|
-
// Fetch products from ForgeCart API
|
|
7
|
-
const result = await forgecart.shop.products.products({
|
|
8
|
-
options: {
|
|
9
|
-
take: 10,
|
|
10
|
-
},
|
|
11
|
-
})
|
|
12
|
-
return (result.products as ProductList) || { items: [], totalItems: 0 }
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error('Failed to fetch products:', error)
|
|
15
|
-
return {
|
|
16
|
-
items: [] as Product[],
|
|
17
|
-
totalItems: 0,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export default async function ProductsPage() {
|
|
23
|
-
const products = await getProducts()
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="container mx-auto px-4 py-8">
|
|
27
|
-
<h1 className="text-3xl font-bold mb-8">Products</h1>
|
|
28
|
-
|
|
29
|
-
{products.items.length === 0 ? (
|
|
30
|
-
<p className="text-gray-600">
|
|
31
|
-
No products found. Make sure your ForgeCart channel is configured
|
|
32
|
-
correctly.
|
|
33
|
-
</p>
|
|
34
|
-
) : (
|
|
35
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
36
|
-
{products.items.map((product: Product) => (
|
|
37
|
-
<div
|
|
38
|
-
key={product.id}
|
|
39
|
-
className="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow"
|
|
40
|
-
>
|
|
41
|
-
{product.featuredAsset && (
|
|
42
|
-
<img
|
|
43
|
-
src={product.featuredAsset.preview}
|
|
44
|
-
alt={product.name}
|
|
45
|
-
className="w-full h-48 object-cover"
|
|
46
|
-
/>
|
|
47
|
-
)}
|
|
48
|
-
<div className="p-4">
|
|
49
|
-
<h2 className="text-lg font-semibold mb-2">{product.name}</h2>
|
|
50
|
-
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
|
|
51
|
-
{product.description}
|
|
52
|
-
</p>
|
|
53
|
-
{product.variants[0] && (
|
|
54
|
-
<p className="text-lg font-bold text-blue-600">
|
|
55
|
-
{(product.variants[0].price / 100).toFixed(2)}{' '}
|
|
56
|
-
{product.variants[0].currencyCode}
|
|
57
|
-
</p>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
))}
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common'
|
|
2
|
-
import { ForgeCartService } from './forgecart.service'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Example service demonstrating ForgeCart SDK integration
|
|
6
|
-
*
|
|
7
|
-
* This can be used for custom backend tasks like:
|
|
8
|
-
* - Scheduled price updates
|
|
9
|
-
* - Inventory synchronization
|
|
10
|
-
* - Custom order processing
|
|
11
|
-
* - Third-party integrations
|
|
12
|
-
*/
|
|
13
|
-
@Injectable()
|
|
14
|
-
export class PriceFetcherService {
|
|
15
|
-
constructor(private readonly forgeCart: ForgeCartService) {}
|
|
16
|
-
|
|
17
|
-
async getProductPrices(productIds: string[]): Promise<
|
|
18
|
-
Array<{
|
|
19
|
-
productId: string
|
|
20
|
-
name: string
|
|
21
|
-
price: number
|
|
22
|
-
}>
|
|
23
|
-
> {
|
|
24
|
-
const prices = []
|
|
25
|
-
|
|
26
|
-
for (const productId of productIds) {
|
|
27
|
-
try {
|
|
28
|
-
const result = await this.forgeCart.shop.products.product({
|
|
29
|
-
id: productId,
|
|
30
|
-
})
|
|
31
|
-
const product = result.product
|
|
32
|
-
if (product && product.variants?.[0]) {
|
|
33
|
-
prices.push({
|
|
34
|
-
productId,
|
|
35
|
-
name: product.name,
|
|
36
|
-
price: product.variants[0].price,
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error(`Failed to fetch price for product ${productId}:`, error)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return prices
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async syncInventory(): Promise<{ synced: number; errors: number }> {
|
|
48
|
-
// Example: Sync inventory with external system
|
|
49
|
-
// This is a placeholder for custom business logic
|
|
50
|
-
let synced = 0
|
|
51
|
-
let errors = 0
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const result = await this.forgeCart.shop.products.products({
|
|
55
|
-
options: { take: 100 },
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
for (const product of result.products.items || []) {
|
|
59
|
-
// Here you would implement your custom sync logic
|
|
60
|
-
// e.g., update external ERP, sync with warehouse system, etc.
|
|
61
|
-
console.log(`Syncing product: ${product.name}`)
|
|
62
|
-
synced++
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error('Inventory sync failed:', error)
|
|
66
|
-
errors++
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return { synced, errors }
|
|
70
|
-
}
|
|
71
|
-
}
|