@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,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
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgecart/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for deploying applications to ForgeCart",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"forgecart": "./bin/forgecart.js"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@forgecart/sdk": "1.2.
|
|
11
|
+
"@forgecart/sdk": "1.2.5"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common'
|
|
2
|
-
import { ConfigModule } from '@nestjs/config'
|
|
3
|
-
import { AppController } from './app.controller'
|
|
4
|
-
import { AppService } from './app.service'
|
|
5
|
-
import { ForgeCartService,
|
|
1
|
+
import { Module } from '@nestjs/common'
|
|
2
|
+
import { ConfigModule } from '@nestjs/config'
|
|
3
|
+
import { AppController } from './app.controller'
|
|
4
|
+
import { AppService } from './app.service'
|
|
5
|
+
import { ForgeCartService, ProductEventListenerService } from '../services'
|
|
6
6
|
|
|
7
7
|
@Module({
|
|
8
8
|
imports: [
|
|
@@ -11,6 +11,6 @@ import { ForgeCartService, PriceFetcherService } from '../services';
|
|
|
11
11
|
}),
|
|
12
12
|
],
|
|
13
13
|
controllers: [AppController],
|
|
14
|
-
providers: [AppService, ForgeCartService,
|
|
14
|
+
providers: [AppService, ForgeCartService, ProductEventListenerService],
|
|
15
15
|
})
|
|
16
16
|
export class AppModule {}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { ForgeCartService } from './forgecart.service'
|
|
2
|
-
export {
|
|
2
|
+
export { ProductEventListenerService } from './product-event-listener.service'
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
OnModuleInit,
|
|
4
|
+
OnModuleDestroy,
|
|
5
|
+
Logger,
|
|
6
|
+
} from '@nestjs/common'
|
|
7
|
+
import { ConfigService } from '@nestjs/config'
|
|
8
|
+
import { ForgeCartService } from './forgecart.service'
|
|
9
|
+
|
|
10
|
+
interface EventSubscription {
|
|
11
|
+
unsubscribe: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Injectable()
|
|
15
|
+
export class ProductEventListenerService
|
|
16
|
+
implements OnModuleInit, OnModuleDestroy
|
|
17
|
+
{
|
|
18
|
+
private readonly logger = new Logger(ProductEventListenerService.name)
|
|
19
|
+
private subscriptions: EventSubscription[] = []
|
|
20
|
+
private isAuthenticated = false
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly forgeCart: ForgeCartService,
|
|
24
|
+
private readonly configService: ConfigService
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
async onModuleInit(): Promise<void> {
|
|
28
|
+
await this.initialize()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onModuleDestroy(): void {
|
|
32
|
+
this.cleanup()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async initialize(): Promise<void> {
|
|
36
|
+
const username = this.configService.get<string>('FORGECART_ADMIN_USERNAME')
|
|
37
|
+
const password = this.configService.get<string>('FORGECART_ADMIN_PASSWORD')
|
|
38
|
+
|
|
39
|
+
// Skip if credentials are not provided
|
|
40
|
+
if (!username || !password) {
|
|
41
|
+
this.logger.warn(
|
|
42
|
+
'FORGECART_ADMIN_USERNAME and FORGECART_ADMIN_PASSWORD not set. ' +
|
|
43
|
+
'Product event listeners will not be registered.'
|
|
44
|
+
)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Login with admin credentials
|
|
50
|
+
const loginResult = await this.forgeCart.admin.administrator.login({
|
|
51
|
+
username,
|
|
52
|
+
password,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const login = (
|
|
56
|
+
loginResult as { login?: { __typename?: string; message?: string } }
|
|
57
|
+
)?.login
|
|
58
|
+
if (login?.__typename !== 'CurrentUser') {
|
|
59
|
+
this.logger.error(
|
|
60
|
+
'Admin login failed:',
|
|
61
|
+
login?.message || 'Unknown error'
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.isAuthenticated = true
|
|
67
|
+
this.logger.log('Successfully authenticated as admin')
|
|
68
|
+
|
|
69
|
+
// Register event handlers
|
|
70
|
+
this.registerEventHandlers()
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.logger.error('Failed to initialize product event listener:', error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private registerEventHandlers(): void {
|
|
77
|
+
if (!this.isAuthenticated) return
|
|
78
|
+
|
|
79
|
+
const sdk = this.forgeCart.getSDK()
|
|
80
|
+
|
|
81
|
+
// Non-blocking handlers
|
|
82
|
+
const productUpdatedSub = sdk.admin.registerEventHandler(
|
|
83
|
+
'product.updated',
|
|
84
|
+
(event) => {
|
|
85
|
+
this.logger.log(`Product updated: ${JSON.stringify(event)}`)
|
|
86
|
+
this.handleProductUpdated(event)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
this.subscriptions.push(productUpdatedSub)
|
|
90
|
+
|
|
91
|
+
const productCreatedSub = sdk.admin.registerEventHandler(
|
|
92
|
+
'product.created',
|
|
93
|
+
(event) => {
|
|
94
|
+
this.logger.log(`Product created: ${JSON.stringify(event)}`)
|
|
95
|
+
this.handleProductCreated(event)
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
this.subscriptions.push(productCreatedSub)
|
|
99
|
+
|
|
100
|
+
const productDeletedSub = sdk.admin.registerEventHandler(
|
|
101
|
+
'product.deleted',
|
|
102
|
+
(event) => {
|
|
103
|
+
this.logger.log(`Product deleted: ${JSON.stringify(event)}`)
|
|
104
|
+
this.handleProductDeleted(event)
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
this.subscriptions.push(productDeletedSub)
|
|
108
|
+
|
|
109
|
+
// Blocking handler (must complete within 1000ms)
|
|
110
|
+
const variantUpdatedSub = sdk.admin.registerBlockingEventHandler(
|
|
111
|
+
'product.variant.updated',
|
|
112
|
+
async (event) => {
|
|
113
|
+
this.logger.log(
|
|
114
|
+
`Product variant updated (blocking): ${JSON.stringify(event)}`
|
|
115
|
+
)
|
|
116
|
+
await this.handleVariantUpdated(event)
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
this.subscriptions.push(variantUpdatedSub)
|
|
120
|
+
|
|
121
|
+
this.logger.log('Product event handlers registered successfully')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private handleProductUpdated(event: unknown): void {
|
|
125
|
+
// Implement custom logic here
|
|
126
|
+
this.logger.debug('handleProductUpdated called', event)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private handleProductCreated(event: unknown): void {
|
|
130
|
+
// Implement custom logic here
|
|
131
|
+
this.logger.debug('handleProductCreated called', event)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private handleProductDeleted(event: unknown): void {
|
|
135
|
+
// Implement custom logic here
|
|
136
|
+
this.logger.debug('handleProductDeleted called', event)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async handleVariantUpdated(event: unknown): Promise<void> {
|
|
140
|
+
// Blocking handler - must complete within 1000ms
|
|
141
|
+
this.logger.debug('handleVariantUpdated called', event)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private cleanup(): void {
|
|
145
|
+
for (const subscription of this.subscriptions) {
|
|
146
|
+
subscription.unsubscribe()
|
|
147
|
+
}
|
|
148
|
+
this.subscriptions = []
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { waitForPortOpen } from '@nx/node/utils'
|
|
2
|
+
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
var __TEARDOWN_MESSAGE__: string
|
|
5
|
+
|
|
6
|
+
module.exports = async function () {
|
|
7
|
+
// Start services that the app needs to run (e.g. database, docker-compose, etc.).
|
|
8
|
+
console.log('\nSetting up...\n')
|
|
9
|
+
|
|
10
|
+
const host = process.env.HOST ?? 'localhost'
|
|
11
|
+
const port = process.env.PORT ? Number(process.env.PORT) : 8080 // Match application's default port
|
|
12
|
+
await waitForPortOpen(port, { host })
|
|
13
|
+
|
|
14
|
+
// Hint: Use `globalThis` to pass variables to global teardown.
|
|
15
|
+
globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
|
|
4
|
+
module.exports = async function () {
|
|
5
|
+
// Configure axios for tests to use.
|
|
6
|
+
const host = process.env.HOST ?? 'localhost'
|
|
7
|
+
const port = process.env.PORT ?? '8080' // Match application's default port
|
|
8
|
+
axios.defaults.baseURL = `http://${host}:${port}`
|
|
9
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { useCart } from '../../context/CartContext'
|
|
5
|
+
import { CartItem } from '../../components/CartItem'
|
|
6
|
+
|
|
7
|
+
export default function CartPage() {
|
|
8
|
+
const { cart, loading, itemCount } = useCart()
|
|
9
|
+
|
|
10
|
+
if (loading && !cart) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
13
|
+
<h1 className="text-3xl font-bold mb-8 text-gray-900">Your Cart</h1>
|
|
14
|
+
<p className="text-gray-600">Loading cart...</p>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!cart || cart.lines.length === 0) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
22
|
+
<h1 className="text-3xl font-bold mb-8 text-gray-900">Your Cart</h1>
|
|
23
|
+
<p className="text-gray-600 mb-4">Your cart is empty.</p>
|
|
24
|
+
<Link
|
|
25
|
+
href="/"
|
|
26
|
+
className="inline-block bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700"
|
|
27
|
+
>
|
|
28
|
+
Continue Shopping
|
|
29
|
+
</Link>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const subtotal = (cart.subTotalWithTax / 100).toFixed(2)
|
|
35
|
+
const total = (cart.totalWithTax / 100).toFixed(2)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
39
|
+
<h1 className="text-3xl font-bold mb-8 text-gray-900">Your Cart</h1>
|
|
40
|
+
|
|
41
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
42
|
+
{cart.lines
|
|
43
|
+
.filter((line) => line.productVariant)
|
|
44
|
+
.map((line) => (
|
|
45
|
+
<CartItem key={line.id} line={line as never} />
|
|
46
|
+
))}
|
|
47
|
+
|
|
48
|
+
<div className="mt-6 pt-4 border-t">
|
|
49
|
+
<div className="flex justify-between text-lg mb-2">
|
|
50
|
+
<span className="text-gray-600">Subtotal ({itemCount} items)</span>
|
|
51
|
+
<span className="font-semibold">
|
|
52
|
+
{subtotal} {cart.currencyCode}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex justify-between text-xl font-bold">
|
|
56
|
+
<span>Total</span>
|
|
57
|
+
<span>
|
|
58
|
+
{total} {cart.currencyCode}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="mt-6 flex gap-4">
|
|
64
|
+
<Link
|
|
65
|
+
href="/"
|
|
66
|
+
className="flex-1 text-center border border-gray-300 py-3 rounded-lg hover:bg-gray-50"
|
|
67
|
+
>
|
|
68
|
+
Continue Shopping
|
|
69
|
+
</Link>
|
|
70
|
+
<button className="flex-1 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700">
|
|
71
|
+
Checkout
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import './global.css'
|
|
2
|
+
import { CartProvider } from '../context/CartContext'
|
|
3
|
+
import { Header } from '../components/Header'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
title: 'Store',
|
|
7
|
+
description: 'Your ecommerce store',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<body className="bg-gray-50 min-h-screen">
|
|
18
|
+
<CartProvider>
|
|
19
|
+
<Header />
|
|
20
|
+
<main>{children}</main>
|
|
21
|
+
</CartProvider>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ProductList, Product } from '@forgecart/sdk/shop'
|
|
2
|
+
import forgecart from '../lib/forgecart'
|
|
3
|
+
import { ProductCard } from '../components/ProductCard'
|
|
4
|
+
|
|
5
|
+
async function getProducts(): Promise<ProductList> {
|
|
6
|
+
try {
|
|
7
|
+
const result = await forgecart.shop.products.products({
|
|
8
|
+
options: {
|
|
9
|
+
take: 20,
|
|
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 HomePage() {
|
|
23
|
+
const products = await getProducts()
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
27
|
+
<h1 className="text-3xl font-bold mb-8 text-gray-900">Our 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 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
36
|
+
{products.items.map((product: Product) => (
|
|
37
|
+
<ProductCard key={product.id} product={product as never} />
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useCart } from '../context/CartContext'
|
|
5
|
+
|
|
6
|
+
interface AddToCartButtonProps {
|
|
7
|
+
productVariantId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AddToCartButton({ productVariantId }: AddToCartButtonProps) {
|
|
11
|
+
const { addToCart } = useCart()
|
|
12
|
+
const [isAdding, setIsAdding] = useState(false)
|
|
13
|
+
|
|
14
|
+
const handleClick = async () => {
|
|
15
|
+
setIsAdding(true)
|
|
16
|
+
await addToCart(productVariantId, 1)
|
|
17
|
+
setIsAdding(false)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
onClick={handleClick}
|
|
23
|
+
disabled={isAdding}
|
|
24
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:bg-blue-300 disabled:cursor-not-allowed transition-colors"
|
|
25
|
+
>
|
|
26
|
+
{isAdding ? 'Adding...' : 'Add to Cart'}
|
|
27
|
+
</button>
|
|
28
|
+
)
|
|
29
|
+
}
|