@forgecart/cli 1.1.0 → 2.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/bin/forgecart.d.ts +2 -0
- package/dist/bin/forgecart.js +4 -0
- package/dist/bin/forgecart.js.map +1 -0
- package/dist/src/cli.d.ts +14 -1
- package/dist/src/cli.js +30 -15
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/init.d.ts +39 -2
- package/dist/src/commands/init.js +173 -301
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/placeholders.d.ts +19 -0
- package/dist/src/commands/placeholders.js +109 -0
- package/dist/src/commands/placeholders.js.map +1 -0
- package/dist/src/output.d.ts +11 -0
- package/dist/src/output.js +16 -0
- package/dist/src/output.js.map +1 -0
- package/package.json +21 -11
- package/templates/storefront/.env.example +12 -0
- package/templates/storefront/.forgecartignore +2 -0
- package/templates/storefront/Procfile +1 -0
- package/templates/storefront/README.md +87 -0
- package/templates/storefront/next.config.js +14 -0
- package/templates/storefront/package.json +25 -0
- package/templates/storefront/postcss.config.js +6 -0
- package/templates/storefront/src/app/cart/page.tsx +88 -0
- package/templates/storefront/src/app/globals.css +13 -0
- package/templates/storefront/src/app/layout.tsx +32 -0
- package/templates/storefront/src/app/page.tsx +49 -0
- package/templates/storefront/src/app/products/[slug]/not-found.tsx +20 -0
- package/templates/storefront/src/app/products/[slug]/page.tsx +61 -0
- package/templates/storefront/src/app/products/page.tsx +27 -0
- package/templates/storefront/src/components/Header.tsx +47 -0
- package/templates/storefront/src/components/ProductCard.tsx +40 -0
- package/templates/storefront/src/components/ProductPurchase.tsx +91 -0
- package/templates/storefront/src/lib/cart-actions.ts +148 -0
- package/templates/storefront/src/lib/cart-context.tsx +88 -0
- package/templates/storefront/src/lib/forgecart.ts +187 -0
- package/{dist/templates/storefront-additions → templates/storefront}/tailwind.config.js +1 -1
- package/templates/storefront/tsconfig.json +23 -0
- package/LICENSE +0 -21
- package/README.md +0 -179
- package/bin/forgecart.js +0 -3
- package/dist/package.json +0 -50
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/commands/delete.d.ts +0 -3
- package/dist/src/commands/delete.d.ts.map +0 -1
- package/dist/src/commands/delete.js +0 -60
- package/dist/src/commands/delete.js.map +0 -1
- package/dist/src/commands/deploy.d.ts +0 -3
- package/dist/src/commands/deploy.d.ts.map +0 -1
- package/dist/src/commands/deploy.js +0 -243
- package/dist/src/commands/deploy.js.map +0 -1
- package/dist/src/commands/env.d.ts +0 -3
- package/dist/src/commands/env.d.ts.map +0 -1
- package/dist/src/commands/env.js +0 -158
- package/dist/src/commands/env.js.map +0 -1
- package/dist/src/commands/init.d.ts.map +0 -1
- package/dist/src/commands/list.d.ts +0 -3
- package/dist/src/commands/list.d.ts.map +0 -1
- package/dist/src/commands/list.js +0 -65
- package/dist/src/commands/list.js.map +0 -1
- package/dist/src/commands/login.d.ts +0 -3
- package/dist/src/commands/login.d.ts.map +0 -1
- package/dist/src/commands/login.js +0 -120
- package/dist/src/commands/login.js.map +0 -1
- package/dist/src/commands/logs.d.ts +0 -3
- package/dist/src/commands/logs.d.ts.map +0 -1
- package/dist/src/commands/logs.js +0 -88
- package/dist/src/commands/logs.js.map +0 -1
- package/dist/src/commands/scale.d.ts +0 -3
- package/dist/src/commands/scale.d.ts.map +0 -1
- package/dist/src/commands/scale.js +0 -49
- package/dist/src/commands/scale.js.map +0 -1
- package/dist/src/commands/status.d.ts +0 -3
- package/dist/src/commands/status.d.ts.map +0 -1
- package/dist/src/commands/status.js +0 -90
- package/dist/src/commands/status.js.map +0 -1
- package/dist/src/commands/whoami.d.ts +0 -3
- package/dist/src/commands/whoami.d.ts.map +0 -1
- package/dist/src/commands/whoami.js +0 -59
- package/dist/src/commands/whoami.js.map +0 -1
- package/dist/src/index.d.ts +0 -3
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -5
- package/dist/src/index.js.map +0 -1
- package/dist/src/lib/api-client.d.ts +0 -34
- package/dist/src/lib/api-client.d.ts.map +0 -1
- package/dist/src/lib/api-client.js +0 -141
- package/dist/src/lib/api-client.js.map +0 -1
- package/dist/src/lib/archive.d.ts +0 -9
- package/dist/src/lib/archive.d.ts.map +0 -1
- package/dist/src/lib/archive.js +0 -118
- package/dist/src/lib/archive.js.map +0 -1
- package/dist/src/lib/config-updater.d.ts +0 -10
- package/dist/src/lib/config-updater.d.ts.map +0 -1
- package/dist/src/lib/config-updater.js +0 -34
- package/dist/src/lib/config-updater.js.map +0 -1
- package/dist/src/lib/config.d.ts +0 -53
- package/dist/src/lib/config.d.ts.map +0 -1
- package/dist/src/lib/config.js +0 -162
- package/dist/src/lib/config.js.map +0 -1
- package/dist/src/lib/env-sync.d.ts +0 -44
- package/dist/src/lib/env-sync.d.ts.map +0 -1
- package/dist/src/lib/env-sync.js +0 -207
- package/dist/src/lib/env-sync.js.map +0 -1
- package/dist/src/lib/logger.d.ts +0 -15
- package/dist/src/lib/logger.d.ts.map +0 -1
- package/dist/src/lib/logger.js +0 -54
- package/dist/src/lib/logger.js.map +0 -1
- package/dist/src/lib/nx-generator.d.ts +0 -22
- package/dist/src/lib/nx-generator.d.ts.map +0 -1
- package/dist/src/lib/nx-generator.js +0 -345
- package/dist/src/lib/nx-generator.js.map +0 -1
- package/dist/src/lib/standalone.d.ts +0 -28
- package/dist/src/lib/standalone.d.ts.map +0 -1
- package/dist/src/lib/standalone.js +0 -107
- package/dist/src/lib/standalone.js.map +0 -1
- package/dist/src/types/index.d.ts +0 -80
- package/dist/src/types/index.d.ts.map +0 -1
- package/dist/src/types/index.js +0 -14
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/utils/spinner.d.ts +0 -16
- package/dist/src/utils/spinner.d.ts.map +0 -1
- package/dist/src/utils/spinner.js +0 -88
- package/dist/src/utils/spinner.js.map +0 -1
- package/dist/src/utils/validation.d.ts +0 -12
- package/dist/src/utils/validation.d.ts.map +0 -1
- package/dist/src/utils/validation.js +0 -82
- package/dist/src/utils/validation.js.map +0 -1
- package/dist/templates/application-additions/.forgecartignore +0 -37
- package/dist/templates/application-additions/Procfile +0 -1
- package/dist/templates/application-additions/src/app/app.module.ts +0 -16
- package/dist/templates/application-additions/src/services/forgecart.service.ts +0 -62
- package/dist/templates/application-additions/src/services/index.ts +0 -2
- package/dist/templates/application-additions/src/services/product-event-listener.service.ts +0 -150
- package/dist/templates/application-e2e-additions/src/support/global-setup.ts +0 -16
- package/dist/templates/application-e2e-additions/src/support/test-setup.ts +0 -9
- package/dist/templates/storefront-additions/.forgecartignore +0 -64
- package/dist/templates/storefront-additions/Procfile +0 -1
- package/dist/templates/storefront-additions/package.json +0 -11
- package/dist/templates/storefront-additions/postcss.config.js +0 -5
- package/dist/templates/storefront-additions/src/app/cart/page.tsx +0 -77
- package/dist/templates/storefront-additions/src/app/global.css +0 -1
- package/dist/templates/storefront-additions/src/app/layout.tsx +0 -25
- package/dist/templates/storefront-additions/src/app/page.tsx +0 -43
- package/dist/templates/storefront-additions/src/components/AddToCartButton.tsx +0 -29
- package/dist/templates/storefront-additions/src/components/CartItem.tsx +0 -82
- package/dist/templates/storefront-additions/src/components/Header.tsx +0 -42
- package/dist/templates/storefront-additions/src/components/ProductCard.tsx +0 -57
- package/dist/templates/storefront-additions/src/components/QuantitySelector.tsx +0 -35
- package/dist/templates/storefront-additions/src/context/CartContext.tsx +0 -158
- package/dist/templates/storefront-additions/src/lib/forgecart.ts +0 -22
- package/dist/templates/storefront-e2e-additions/playwright.config.ts +0 -26
- package/dist/templates/storefront-e2e-additions/src/example.spec.ts +0 -11
- package/dist/templates/workspace/.nxignore +0 -2
- package/templates/application-additions/.forgecartignore +0 -37
- package/templates/application-additions/Procfile +0 -1
- package/templates/application-additions/src/app/app.module.ts +0 -16
- package/templates/application-additions/src/services/forgecart.service.ts +0 -62
- package/templates/application-additions/src/services/index.ts +0 -2
- package/templates/application-additions/src/services/product-event-listener.service.ts +0 -150
- package/templates/application-e2e-additions/src/support/global-setup.ts +0 -16
- package/templates/application-e2e-additions/src/support/test-setup.ts +0 -9
- package/templates/storefront-additions/.forgecartignore +0 -64
- package/templates/storefront-additions/Procfile +0 -1
- package/templates/storefront-additions/package.json +0 -11
- package/templates/storefront-additions/postcss.config.js +0 -5
- package/templates/storefront-additions/src/app/cart/page.tsx +0 -77
- package/templates/storefront-additions/src/app/global.css +0 -1
- package/templates/storefront-additions/src/app/layout.tsx +0 -25
- package/templates/storefront-additions/src/app/page.tsx +0 -43
- package/templates/storefront-additions/src/components/AddToCartButton.tsx +0 -29
- package/templates/storefront-additions/src/components/CartItem.tsx +0 -82
- package/templates/storefront-additions/src/components/Header.tsx +0 -42
- package/templates/storefront-additions/src/components/ProductCard.tsx +0 -57
- package/templates/storefront-additions/src/components/QuantitySelector.tsx +0 -35
- package/templates/storefront-additions/src/context/CartContext.tsx +0 -158
- package/templates/storefront-additions/src/lib/forgecart.ts +0 -22
- package/templates/storefront-additions/tailwind.config.js +0 -8
- package/templates/storefront-e2e-additions/playwright.config.ts +0 -26
- package/templates/storefront-e2e-additions/src/example.spec.ts +0 -11
- package/templates/workspace/.nxignore +0 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { print } from '../output.js';
|
|
3
|
+
/**
|
|
4
|
+
* Commands whose behaviour ships with the publish pipeline. They are registered
|
|
5
|
+
* now so the command surface and `--help` text exist, but their actions only
|
|
6
|
+
* announce that the implementation is not yet wired in v2.
|
|
7
|
+
*/
|
|
8
|
+
function notImplemented(command) {
|
|
9
|
+
print(`\`forgecart ${command}\` is not implemented yet in v2. ` +
|
|
10
|
+
'It arrives with the publish pipeline.');
|
|
11
|
+
}
|
|
12
|
+
/** `init:application` — add a NestJS application to an existing project. */
|
|
13
|
+
export function createInitApplicationCommand() {
|
|
14
|
+
return new Command('init:application')
|
|
15
|
+
.description('Add a NestJS application to an existing ForgeCart project')
|
|
16
|
+
.action(() => {
|
|
17
|
+
notImplemented('init:application');
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** `deploy` — deploy the current project. */
|
|
21
|
+
export function createDeployCommand() {
|
|
22
|
+
return new Command('deploy')
|
|
23
|
+
.description('Deploy the current ForgeCart project')
|
|
24
|
+
.option('--skip-build', 'Skip the build step')
|
|
25
|
+
.action(() => {
|
|
26
|
+
notImplemented('deploy');
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** `list` — list deployments for the channel. */
|
|
30
|
+
export function createListCommand() {
|
|
31
|
+
return new Command('list')
|
|
32
|
+
.alias('ls')
|
|
33
|
+
.description('List all deployments for this channel')
|
|
34
|
+
.action(() => {
|
|
35
|
+
notImplemented('list');
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** `status [type]` — show deployment status. */
|
|
39
|
+
export function createStatusCommand() {
|
|
40
|
+
return new Command('status')
|
|
41
|
+
.description('Show detailed deployment status')
|
|
42
|
+
.argument('[type]', 'Deployment type (storefront or application)')
|
|
43
|
+
.action(() => {
|
|
44
|
+
notImplemented('status');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** `logs [type]` — view deployment logs. */
|
|
48
|
+
export function createLogsCommand() {
|
|
49
|
+
return new Command('logs')
|
|
50
|
+
.description('View deployment logs')
|
|
51
|
+
.argument('[type]', 'Deployment type (storefront or application)', 'storefront')
|
|
52
|
+
.option('-f, --follow', 'Follow log output')
|
|
53
|
+
.option('-n, --lines <number>', 'Number of lines to show', '100')
|
|
54
|
+
.action(() => {
|
|
55
|
+
notImplemented('logs');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** `delete <type>` — delete a deployment. */
|
|
59
|
+
export function createDeleteCommand() {
|
|
60
|
+
return new Command('delete')
|
|
61
|
+
.description('Delete a deployment')
|
|
62
|
+
.argument('<type>', 'Deployment type (storefront or application)')
|
|
63
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
64
|
+
.action(() => {
|
|
65
|
+
notImplemented('delete');
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** `scale <type> <instances>` — scale a deployment. */
|
|
69
|
+
export function createScaleCommand() {
|
|
70
|
+
return new Command('scale')
|
|
71
|
+
.description('Scale a deployment')
|
|
72
|
+
.argument('<type>', 'Deployment type (storefront or application)')
|
|
73
|
+
.argument('<instances>', 'Number of instances (0-10)')
|
|
74
|
+
.action(() => {
|
|
75
|
+
notImplemented('scale');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** `env` — manage environment variables (get/set/unset). */
|
|
79
|
+
export function createEnvCommand() {
|
|
80
|
+
const cmd = new Command('env').description('Manage deployment environment variables');
|
|
81
|
+
cmd
|
|
82
|
+
.command('get <type>')
|
|
83
|
+
.description('Get environment variables for a deployment')
|
|
84
|
+
.action(() => {
|
|
85
|
+
notImplemented('env get');
|
|
86
|
+
});
|
|
87
|
+
cmd
|
|
88
|
+
.command('set <type> <key> <value>')
|
|
89
|
+
.description('Set an environment variable')
|
|
90
|
+
.action(() => {
|
|
91
|
+
notImplemented('env set');
|
|
92
|
+
});
|
|
93
|
+
cmd
|
|
94
|
+
.command('unset <type> <key>')
|
|
95
|
+
.description('Remove an environment variable')
|
|
96
|
+
.action(() => {
|
|
97
|
+
notImplemented('env unset');
|
|
98
|
+
});
|
|
99
|
+
return cmd;
|
|
100
|
+
}
|
|
101
|
+
/** `whoami` — show current channel information. */
|
|
102
|
+
export function createWhoamiCommand() {
|
|
103
|
+
return new Command('whoami')
|
|
104
|
+
.description('Show current channel information')
|
|
105
|
+
.action(() => {
|
|
106
|
+
notImplemented('whoami');
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=placeholders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholders.js","sourceRoot":"","sources":["../../../src/commands/placeholders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal stdout/stderr writers for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* A publishable CLI legitimately prints to standard streams, but the repo lint
|
|
5
|
+
* rules forbid `console.*`. These helpers centralise process stream access so
|
|
6
|
+
* the rest of the CLI never touches `console`.
|
|
7
|
+
*/
|
|
8
|
+
/** Write a line to standard output. */
|
|
9
|
+
export declare function print(message?: string): void;
|
|
10
|
+
/** Write a line to standard error. */
|
|
11
|
+
export declare function printError(message: string): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal stdout/stderr writers for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* A publishable CLI legitimately prints to standard streams, but the repo lint
|
|
5
|
+
* rules forbid `console.*`. These helpers centralise process stream access so
|
|
6
|
+
* the rest of the CLI never touches `console`.
|
|
7
|
+
*/
|
|
8
|
+
/** Write a line to standard output. */
|
|
9
|
+
export function print(message = '') {
|
|
10
|
+
process.stdout.write(`${message}\n`);
|
|
11
|
+
}
|
|
12
|
+
/** Write a line to standard error. */
|
|
13
|
+
export function printError(message) {
|
|
14
|
+
process.stderr.write(`${message}\n`);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/output.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,uCAAuC;AACvC,MAAM,UAAU,KAAK,CAAC,OAAO,GAAG,EAAE;IAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;AACvC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgecart/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "CLI
|
|
6
|
-
"main": "dist/index.js",
|
|
5
|
+
"description": "CLI for scaffolding and operating ForgeCart channel storefronts",
|
|
7
6
|
"bin": {
|
|
8
|
-
"forgecart": "./bin/forgecart.js"
|
|
7
|
+
"forgecart": "./dist/bin/forgecart.js"
|
|
9
8
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
9
|
+
"main": "./dist/src/cli.js",
|
|
10
|
+
"types": "./dist/src/cli.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/src/cli.d.ts",
|
|
14
|
+
"import": "./dist/src/cli.js",
|
|
15
|
+
"default": "./dist/src/cli.js"
|
|
16
|
+
}
|
|
12
17
|
},
|
|
13
18
|
"files": [
|
|
14
19
|
"dist",
|
|
15
|
-
"bin",
|
|
16
20
|
"templates"
|
|
17
21
|
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^12.1.0"
|
|
24
|
+
},
|
|
18
25
|
"engines": {
|
|
19
26
|
"node": ">=18.0.0"
|
|
20
27
|
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
21
31
|
"keywords": [
|
|
22
32
|
"forgecart",
|
|
23
33
|
"cli",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
34
|
+
"storefront",
|
|
35
|
+
"scaffold",
|
|
36
|
+
"nextjs"
|
|
27
37
|
],
|
|
28
38
|
"author": "ForgeCart",
|
|
29
39
|
"license": "MIT"
|
|
30
|
-
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# ForgeCart channel credentials.
|
|
2
|
+
#
|
|
3
|
+
# `forgecart init` writes the real values to `.env.local` for you. This file
|
|
4
|
+
# documents the variables the storefront reads. Both are required and used
|
|
5
|
+
# server-side only (never exposed to the browser).
|
|
6
|
+
|
|
7
|
+
# The channel token identifies which ForgeCart channel this storefront serves.
|
|
8
|
+
# It is sent as the `forgecart-token` header on every shop API request.
|
|
9
|
+
FORGECART_CHANNEL_TOKEN=
|
|
10
|
+
|
|
11
|
+
# Base URL of the ForgeCart shop GraphQL API (the shop-api endpoint).
|
|
12
|
+
FORGECART_SHOP_API_URL=https://api.forgecart.com/shop-api
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: npm run start
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Storefront
|
|
2
|
+
|
|
3
|
+
A minimal [Next.js 15](https://nextjs.org) (App Router) storefront for a
|
|
4
|
+
ForgeCart channel. It talks to the ForgeCart shop GraphQL API through
|
|
5
|
+
`@forgecart/sdk` and renders product data in Server Components.
|
|
6
|
+
|
|
7
|
+
This directory is the scaffold copied verbatim into your project by
|
|
8
|
+
`forgecart init`. Nothing here is compiled by the ForgeCart monorepo; it ships
|
|
9
|
+
as-is in the published `@forgecart/cli` package.
|
|
10
|
+
|
|
11
|
+
## How the channel token + API URL wire in
|
|
12
|
+
|
|
13
|
+
The storefront reads two environment variables (server-side only):
|
|
14
|
+
|
|
15
|
+
| Variable | Purpose |
|
|
16
|
+
| ------------------------- | ------------------------------------------------------------- |
|
|
17
|
+
| `FORGECART_CHANNEL_TOKEN` | Identifies the channel; sent as the `forgecart-token` header. |
|
|
18
|
+
| `FORGECART_SHOP_API_URL` | Base URL of the channel's shop GraphQL API (`.../shop-api`). |
|
|
19
|
+
|
|
20
|
+
`forgecart init` writes both to `.env.local` for you. To configure manually,
|
|
21
|
+
copy `.env.example` to `.env.local` and fill them in.
|
|
22
|
+
|
|
23
|
+
These values are consumed in [`src/lib/forgecart.ts`](src/lib/forgecart.ts),
|
|
24
|
+
which constructs and memoizes a singleton `ForgeCartShopClient`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
new ForgeCartShopClient({
|
|
28
|
+
endpoint: process.env.FORGECART_SHOP_API_URL,
|
|
29
|
+
channelToken: process.env.FORGECART_CHANNEL_TOKEN,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Because the client is only imported from Server Components, the channel token
|
|
34
|
+
never reaches the browser.
|
|
35
|
+
|
|
36
|
+
## Running locally
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install
|
|
40
|
+
npm run dev # next dev -> http://localhost:3000
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A ForgeCart channel workspace serves this app as its live site by running
|
|
44
|
+
`npm run dev` (development) or `npm run build && npm run start` (production; see
|
|
45
|
+
the `Procfile`).
|
|
46
|
+
|
|
47
|
+
## Project layout
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
src/
|
|
51
|
+
lib/
|
|
52
|
+
forgecart.ts # shop client singleton + typed product helpers
|
|
53
|
+
cart-context.tsx # local (localStorage) cart state for the MVP
|
|
54
|
+
components/
|
|
55
|
+
Header.tsx # top nav: Home / Products / Cart
|
|
56
|
+
ProductCard.tsx # product tile
|
|
57
|
+
AddToCartButton.tsx
|
|
58
|
+
app/
|
|
59
|
+
layout.tsx # html shell + header/footer + CartProvider
|
|
60
|
+
page.tsx # home: greeting + featured products
|
|
61
|
+
products/
|
|
62
|
+
page.tsx # product grid
|
|
63
|
+
[slug]/page.tsx # product detail
|
|
64
|
+
cart/page.tsx # local cart
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Data access
|
|
68
|
+
|
|
69
|
+
`src/lib/forgecart.ts` exposes typed helpers over the shop client:
|
|
70
|
+
|
|
71
|
+
- `getProducts({ take, skip })` -> `{ items, totalItems }`
|
|
72
|
+
- `getFeaturedProducts(count)` -> `StorefrontProduct[]`
|
|
73
|
+
- `getProductBySlug(slug)` / `getProductById(id)` -> `StorefrontProduct | null`
|
|
74
|
+
- `getStartingPrice(product)` and `formatPrice(minorUnits, currency)`
|
|
75
|
+
|
|
76
|
+
The generated typed product operations (`shopClient.product.shopProducts` /
|
|
77
|
+
`.shopProduct`) only select `id` + `optionGroups`, so these helpers use the
|
|
78
|
+
SDK's supported raw-query escape hatch (`shopClient.query<T>(document, vars)`)
|
|
79
|
+
to also fetch `name`, `slug`, `description`, `featuredAsset`, and `variants`
|
|
80
|
+
(price in minor units). Every selected field exists on the shop schema's
|
|
81
|
+
`Product` type — extend the documents in `forgecart.ts` to fetch more.
|
|
82
|
+
|
|
83
|
+
## Cart
|
|
84
|
+
|
|
85
|
+
The starter cart (`src/lib/cart-context.tsx` + `app/cart/page.tsx`) keeps state
|
|
86
|
+
in the browser via `localStorage`. To take real orders, wire it to the
|
|
87
|
+
ForgeCart `cart` / `checkout` shop operations exposed by the SDK.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
// Produce a self-contained build so a channel workspace can run the
|
|
4
|
+
// storefront with `next start` without a full node_modules tree.
|
|
5
|
+
output: 'standalone',
|
|
6
|
+
reactStrictMode: true,
|
|
7
|
+
images: {
|
|
8
|
+
// The shop API serves product/asset previews from arbitrary hosts, so
|
|
9
|
+
// allow any remote image. Tighten this to your asset host in production.
|
|
10
|
+
remotePatterns: [{ protocol: 'https', hostname: '**' }],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = nextConfig;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "storefront",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@forgecart/sdk": "^1.2.6",
|
|
12
|
+
"next": "^15.1.0",
|
|
13
|
+
"react": "^19.0.0",
|
|
14
|
+
"react-dom": "^19.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.10.0",
|
|
18
|
+
"@types/react": "^19.0.0",
|
|
19
|
+
"@types/react-dom": "^19.0.0",
|
|
20
|
+
"autoprefixer": "^10.4.20",
|
|
21
|
+
"postcss": "^8.4.49",
|
|
22
|
+
"tailwindcss": "^3.4.17",
|
|
23
|
+
"typescript": "^5.7.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
|
|
5
|
+
import { useCart } from '../../lib/cart-context';
|
|
6
|
+
import { formatPrice } from '../../lib/forgecart';
|
|
7
|
+
|
|
8
|
+
export default function CartPage() {
|
|
9
|
+
const { cart, itemCount, subtotal, setQuantity, remove, pending } = useCart();
|
|
10
|
+
const lines = cart?.lines ?? [];
|
|
11
|
+
|
|
12
|
+
if (itemCount === 0) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-4">
|
|
15
|
+
<h1 className="text-2xl font-bold tracking-tight text-gray-900">Your cart</h1>
|
|
16
|
+
<p className="text-gray-500">Your cart is empty.</p>
|
|
17
|
+
<Link
|
|
18
|
+
href="/products"
|
|
19
|
+
className="inline-block rounded-md bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
20
|
+
>
|
|
21
|
+
Browse products
|
|
22
|
+
</Link>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="space-y-6">
|
|
29
|
+
<h1 className="text-2xl font-bold tracking-tight text-gray-900">Your cart</h1>
|
|
30
|
+
|
|
31
|
+
<ul className="divide-y divide-gray-200 rounded-lg border border-gray-200 bg-white">
|
|
32
|
+
{lines.map((line) => (
|
|
33
|
+
<li key={line.id} className="flex items-center gap-4 p-4">
|
|
34
|
+
<div className="h-16 w-16 shrink-0 overflow-hidden rounded bg-gray-100">
|
|
35
|
+
{line.featuredAsset?.preview ? (
|
|
36
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
37
|
+
<img
|
|
38
|
+
src={line.featuredAsset.preview}
|
|
39
|
+
alt={line.productVariant.name}
|
|
40
|
+
className="h-full w-full object-cover"
|
|
41
|
+
/>
|
|
42
|
+
) : null}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="min-w-0 flex-1">
|
|
46
|
+
<p className="truncate font-medium text-gray-900">{line.productVariant.name}</p>
|
|
47
|
+
<p className="text-sm text-gray-500">{formatPrice(line.unitPriceWithTax)} each</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<label className="sr-only" htmlFor={`qty-${line.id}`}>
|
|
52
|
+
Quantity
|
|
53
|
+
</label>
|
|
54
|
+
<input
|
|
55
|
+
id={`qty-${line.id}`}
|
|
56
|
+
type="number"
|
|
57
|
+
min={1}
|
|
58
|
+
value={line.quantity}
|
|
59
|
+
disabled={pending}
|
|
60
|
+
onChange={(e) => setQuantity(line.id, Number.parseInt(e.target.value, 10) || 1)}
|
|
61
|
+
className="w-16 rounded border border-gray-300 px-2 py-1 text-sm disabled:opacity-50"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="w-24 text-right font-medium text-gray-900">
|
|
66
|
+
{formatPrice(line.linePriceWithTax)}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={() => remove(line.id)}
|
|
72
|
+
disabled={pending}
|
|
73
|
+
className="text-sm text-gray-400 hover:text-red-600 disabled:opacity-50"
|
|
74
|
+
aria-label={`Remove ${line.productVariant.name}`}
|
|
75
|
+
>
|
|
76
|
+
Remove
|
|
77
|
+
</button>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
|
|
82
|
+
<div className="flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4">
|
|
83
|
+
<span className="text-sm text-gray-600">Subtotal ({itemCount} items)</span>
|
|
84
|
+
<span className="text-lg font-semibold text-gray-900">{formatPrice(subtotal)}</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Header } from '../components/Header';
|
|
5
|
+
import { getCart } from '../lib/cart-actions';
|
|
6
|
+
import { CartProvider } from '../lib/cart-context';
|
|
7
|
+
|
|
8
|
+
import './globals.css';
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = {
|
|
11
|
+
title: 'Storefront',
|
|
12
|
+
description: 'A ForgeCart channel storefront.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
16
|
+
const initialCart = await getCart();
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body className="min-h-screen bg-gray-50 text-gray-900 antialiased">
|
|
20
|
+
<CartProvider initialCart={initialCart}>
|
|
21
|
+
<Header />
|
|
22
|
+
<main className="mx-auto max-w-6xl px-4 py-8">{children}</main>
|
|
23
|
+
<footer className="border-t border-gray-200 bg-white">
|
|
24
|
+
<div className="mx-auto max-w-6xl px-4 py-6 text-sm text-gray-500">
|
|
25
|
+
Powered by ForgeCart
|
|
26
|
+
</div>
|
|
27
|
+
</footer>
|
|
28
|
+
</CartProvider>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
import { ProductCard } from '../components/ProductCard';
|
|
4
|
+
import { getFeaturedProducts } from '../lib/forgecart';
|
|
5
|
+
|
|
6
|
+
// Product data changes in the dashboard, so render on each request.
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
export default async function HomePage() {
|
|
10
|
+
const products = await getFeaturedProducts(4);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-12">
|
|
14
|
+
<section className="rounded-2xl bg-white p-10 shadow-sm">
|
|
15
|
+
<h1 className="text-3xl font-bold tracking-tight text-gray-900">
|
|
16
|
+
Welcome to the shop
|
|
17
|
+
</h1>
|
|
18
|
+
<p className="mt-3 max-w-2xl text-gray-600">
|
|
19
|
+
This storefront is served by a ForgeCart channel. Browse the catalog
|
|
20
|
+
and add products to your cart.
|
|
21
|
+
</p>
|
|
22
|
+
<Link
|
|
23
|
+
href="/products"
|
|
24
|
+
className="mt-6 inline-block rounded-md bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
25
|
+
>
|
|
26
|
+
Shop all products
|
|
27
|
+
</Link>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<section>
|
|
31
|
+
<div className="mb-4 flex items-center justify-between">
|
|
32
|
+
<h2 className="text-xl font-semibold text-gray-900">Featured</h2>
|
|
33
|
+
<Link href="/products" className="text-sm text-gray-600 hover:text-gray-900">
|
|
34
|
+
View all
|
|
35
|
+
</Link>
|
|
36
|
+
</div>
|
|
37
|
+
{products.length === 0 ? (
|
|
38
|
+
<p className="text-gray-500">No products yet. Add some in your dashboard.</p>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
|
41
|
+
{products.map((product) => (
|
|
42
|
+
<ProductCard key={product.id} product={product} />
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</section>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
export default function ProductNotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="space-y-4">
|
|
6
|
+
<h1 className="text-2xl font-bold tracking-tight text-gray-900">
|
|
7
|
+
Product not found
|
|
8
|
+
</h1>
|
|
9
|
+
<p className="text-gray-500">
|
|
10
|
+
We couldn't find that product. It may have been removed.
|
|
11
|
+
</p>
|
|
12
|
+
<Link
|
|
13
|
+
href="/products"
|
|
14
|
+
className="inline-block rounded-md bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
15
|
+
>
|
|
16
|
+
Back to products
|
|
17
|
+
</Link>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
|
|
4
|
+
import { ProductPurchase } from '../../../components/ProductPurchase';
|
|
5
|
+
import { getProductBySlug } from '../../../lib/forgecart';
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic';
|
|
8
|
+
|
|
9
|
+
// Next.js 15: route params are delivered as a Promise.
|
|
10
|
+
export default async function ProductDetailPage({
|
|
11
|
+
params,
|
|
12
|
+
}: {
|
|
13
|
+
params: Promise<{ slug: string }>;
|
|
14
|
+
}) {
|
|
15
|
+
const { slug } = await params;
|
|
16
|
+
const product = await getProductBySlug(slug);
|
|
17
|
+
|
|
18
|
+
if (!product) {
|
|
19
|
+
notFound();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-6">
|
|
24
|
+
<Link href="/products" className="text-sm text-gray-600 hover:text-gray-900">
|
|
25
|
+
← Back to products
|
|
26
|
+
</Link>
|
|
27
|
+
|
|
28
|
+
<div className="grid gap-8 md:grid-cols-2">
|
|
29
|
+
<div className="aspect-square overflow-hidden rounded-lg border border-gray-200 bg-gray-100">
|
|
30
|
+
{product.featuredAsset ? (
|
|
31
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
32
|
+
<img
|
|
33
|
+
src={product.featuredAsset.preview}
|
|
34
|
+
alt={product.name}
|
|
35
|
+
className="h-full w-full object-cover"
|
|
36
|
+
/>
|
|
37
|
+
) : (
|
|
38
|
+
<div className="flex h-full w-full items-center justify-center text-sm text-gray-400">
|
|
39
|
+
No image
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="space-y-4">
|
|
45
|
+
<h1 className="text-2xl font-bold tracking-tight text-gray-900">{product.name}</h1>
|
|
46
|
+
|
|
47
|
+
{product.description && (
|
|
48
|
+
<div
|
|
49
|
+
className="prose prose-sm max-w-none text-gray-600"
|
|
50
|
+
// Product descriptions may contain rich text from the dashboard.
|
|
51
|
+
dangerouslySetInnerHTML={{ __html: product.description }}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{/* Pick a variant (Size/Color) -> price + add-to-cart. */}
|
|
56
|
+
<ProductPurchase product={product} />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ProductCard } from '../../components/ProductCard';
|
|
2
|
+
import { getProducts } from '../../lib/forgecart';
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic';
|
|
5
|
+
|
|
6
|
+
export default async function ProductsPage() {
|
|
7
|
+
const { items, totalItems } = await getProducts({ take: 24 });
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="space-y-6">
|
|
11
|
+
<div className="flex items-baseline justify-between">
|
|
12
|
+
<h1 className="text-2xl font-bold tracking-tight text-gray-900">Products</h1>
|
|
13
|
+
<span className="text-sm text-gray-500">{totalItems} items</span>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
{items.length === 0 ? (
|
|
17
|
+
<p className="text-gray-500">No products yet. Add some in your dashboard.</p>
|
|
18
|
+
) : (
|
|
19
|
+
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
|
20
|
+
{items.map((product) => (
|
|
21
|
+
<ProductCard key={product.id} product={product} />
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|