@forgecart/cli 1.1.0 → 2.0.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.
Files changed (180) hide show
  1. package/dist/bin/forgecart.d.ts +2 -0
  2. package/dist/bin/forgecart.js +4 -0
  3. package/dist/bin/forgecart.js.map +1 -0
  4. package/dist/src/cli.d.ts +14 -1
  5. package/dist/src/cli.js +30 -15
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/commands/init.d.ts +20 -2
  8. package/dist/src/commands/init.js +88 -307
  9. package/dist/src/commands/init.js.map +1 -1
  10. package/dist/src/commands/placeholders.d.ts +19 -0
  11. package/dist/src/commands/placeholders.js +109 -0
  12. package/dist/src/commands/placeholders.js.map +1 -0
  13. package/dist/src/output.d.ts +11 -0
  14. package/dist/src/output.js +16 -0
  15. package/dist/src/output.js.map +1 -0
  16. package/package.json +21 -11
  17. package/templates/storefront/.env.example +12 -0
  18. package/templates/storefront/.forgecartignore +2 -0
  19. package/templates/storefront/Procfile +1 -0
  20. package/templates/storefront/README.md +87 -0
  21. package/templates/storefront/next.config.js +14 -0
  22. package/templates/storefront/package.json +25 -0
  23. package/templates/storefront/postcss.config.js +6 -0
  24. package/templates/storefront/src/app/cart/page.tsx +110 -0
  25. package/templates/storefront/src/app/globals.css +13 -0
  26. package/templates/storefront/src/app/layout.tsx +30 -0
  27. package/templates/storefront/src/app/page.tsx +49 -0
  28. package/templates/storefront/src/app/products/[slug]/not-found.tsx +20 -0
  29. package/templates/storefront/src/app/products/[slug]/page.tsx +96 -0
  30. package/templates/storefront/src/app/products/page.tsx +27 -0
  31. package/templates/storefront/src/components/AddToCartButton.tsx +30 -0
  32. package/templates/storefront/src/components/Header.tsx +36 -0
  33. package/templates/storefront/src/components/ProductCard.tsx +43 -0
  34. package/templates/storefront/src/lib/cart-context.tsx +116 -0
  35. package/templates/storefront/src/lib/forgecart.ts +202 -0
  36. package/{dist/templates/storefront-additions → templates/storefront}/tailwind.config.js +1 -1
  37. package/templates/storefront/tsconfig.json +23 -0
  38. package/LICENSE +0 -21
  39. package/README.md +0 -179
  40. package/bin/forgecart.js +0 -3
  41. package/dist/package.json +0 -50
  42. package/dist/src/cli.d.ts.map +0 -1
  43. package/dist/src/commands/delete.d.ts +0 -3
  44. package/dist/src/commands/delete.d.ts.map +0 -1
  45. package/dist/src/commands/delete.js +0 -60
  46. package/dist/src/commands/delete.js.map +0 -1
  47. package/dist/src/commands/deploy.d.ts +0 -3
  48. package/dist/src/commands/deploy.d.ts.map +0 -1
  49. package/dist/src/commands/deploy.js +0 -243
  50. package/dist/src/commands/deploy.js.map +0 -1
  51. package/dist/src/commands/env.d.ts +0 -3
  52. package/dist/src/commands/env.d.ts.map +0 -1
  53. package/dist/src/commands/env.js +0 -158
  54. package/dist/src/commands/env.js.map +0 -1
  55. package/dist/src/commands/init.d.ts.map +0 -1
  56. package/dist/src/commands/list.d.ts +0 -3
  57. package/dist/src/commands/list.d.ts.map +0 -1
  58. package/dist/src/commands/list.js +0 -65
  59. package/dist/src/commands/list.js.map +0 -1
  60. package/dist/src/commands/login.d.ts +0 -3
  61. package/dist/src/commands/login.d.ts.map +0 -1
  62. package/dist/src/commands/login.js +0 -120
  63. package/dist/src/commands/login.js.map +0 -1
  64. package/dist/src/commands/logs.d.ts +0 -3
  65. package/dist/src/commands/logs.d.ts.map +0 -1
  66. package/dist/src/commands/logs.js +0 -88
  67. package/dist/src/commands/logs.js.map +0 -1
  68. package/dist/src/commands/scale.d.ts +0 -3
  69. package/dist/src/commands/scale.d.ts.map +0 -1
  70. package/dist/src/commands/scale.js +0 -49
  71. package/dist/src/commands/scale.js.map +0 -1
  72. package/dist/src/commands/status.d.ts +0 -3
  73. package/dist/src/commands/status.d.ts.map +0 -1
  74. package/dist/src/commands/status.js +0 -90
  75. package/dist/src/commands/status.js.map +0 -1
  76. package/dist/src/commands/whoami.d.ts +0 -3
  77. package/dist/src/commands/whoami.d.ts.map +0 -1
  78. package/dist/src/commands/whoami.js +0 -59
  79. package/dist/src/commands/whoami.js.map +0 -1
  80. package/dist/src/index.d.ts +0 -3
  81. package/dist/src/index.d.ts.map +0 -1
  82. package/dist/src/index.js +0 -5
  83. package/dist/src/index.js.map +0 -1
  84. package/dist/src/lib/api-client.d.ts +0 -34
  85. package/dist/src/lib/api-client.d.ts.map +0 -1
  86. package/dist/src/lib/api-client.js +0 -141
  87. package/dist/src/lib/api-client.js.map +0 -1
  88. package/dist/src/lib/archive.d.ts +0 -9
  89. package/dist/src/lib/archive.d.ts.map +0 -1
  90. package/dist/src/lib/archive.js +0 -118
  91. package/dist/src/lib/archive.js.map +0 -1
  92. package/dist/src/lib/config-updater.d.ts +0 -10
  93. package/dist/src/lib/config-updater.d.ts.map +0 -1
  94. package/dist/src/lib/config-updater.js +0 -34
  95. package/dist/src/lib/config-updater.js.map +0 -1
  96. package/dist/src/lib/config.d.ts +0 -53
  97. package/dist/src/lib/config.d.ts.map +0 -1
  98. package/dist/src/lib/config.js +0 -162
  99. package/dist/src/lib/config.js.map +0 -1
  100. package/dist/src/lib/env-sync.d.ts +0 -44
  101. package/dist/src/lib/env-sync.d.ts.map +0 -1
  102. package/dist/src/lib/env-sync.js +0 -207
  103. package/dist/src/lib/env-sync.js.map +0 -1
  104. package/dist/src/lib/logger.d.ts +0 -15
  105. package/dist/src/lib/logger.d.ts.map +0 -1
  106. package/dist/src/lib/logger.js +0 -54
  107. package/dist/src/lib/logger.js.map +0 -1
  108. package/dist/src/lib/nx-generator.d.ts +0 -22
  109. package/dist/src/lib/nx-generator.d.ts.map +0 -1
  110. package/dist/src/lib/nx-generator.js +0 -345
  111. package/dist/src/lib/nx-generator.js.map +0 -1
  112. package/dist/src/lib/standalone.d.ts +0 -28
  113. package/dist/src/lib/standalone.d.ts.map +0 -1
  114. package/dist/src/lib/standalone.js +0 -107
  115. package/dist/src/lib/standalone.js.map +0 -1
  116. package/dist/src/types/index.d.ts +0 -80
  117. package/dist/src/types/index.d.ts.map +0 -1
  118. package/dist/src/types/index.js +0 -14
  119. package/dist/src/types/index.js.map +0 -1
  120. package/dist/src/utils/spinner.d.ts +0 -16
  121. package/dist/src/utils/spinner.d.ts.map +0 -1
  122. package/dist/src/utils/spinner.js +0 -88
  123. package/dist/src/utils/spinner.js.map +0 -1
  124. package/dist/src/utils/validation.d.ts +0 -12
  125. package/dist/src/utils/validation.d.ts.map +0 -1
  126. package/dist/src/utils/validation.js +0 -82
  127. package/dist/src/utils/validation.js.map +0 -1
  128. package/dist/templates/application-additions/.forgecartignore +0 -37
  129. package/dist/templates/application-additions/Procfile +0 -1
  130. package/dist/templates/application-additions/src/app/app.module.ts +0 -16
  131. package/dist/templates/application-additions/src/services/forgecart.service.ts +0 -62
  132. package/dist/templates/application-additions/src/services/index.ts +0 -2
  133. package/dist/templates/application-additions/src/services/product-event-listener.service.ts +0 -150
  134. package/dist/templates/application-e2e-additions/src/support/global-setup.ts +0 -16
  135. package/dist/templates/application-e2e-additions/src/support/test-setup.ts +0 -9
  136. package/dist/templates/storefront-additions/.forgecartignore +0 -64
  137. package/dist/templates/storefront-additions/Procfile +0 -1
  138. package/dist/templates/storefront-additions/package.json +0 -11
  139. package/dist/templates/storefront-additions/postcss.config.js +0 -5
  140. package/dist/templates/storefront-additions/src/app/cart/page.tsx +0 -77
  141. package/dist/templates/storefront-additions/src/app/global.css +0 -1
  142. package/dist/templates/storefront-additions/src/app/layout.tsx +0 -25
  143. package/dist/templates/storefront-additions/src/app/page.tsx +0 -43
  144. package/dist/templates/storefront-additions/src/components/AddToCartButton.tsx +0 -29
  145. package/dist/templates/storefront-additions/src/components/CartItem.tsx +0 -82
  146. package/dist/templates/storefront-additions/src/components/Header.tsx +0 -42
  147. package/dist/templates/storefront-additions/src/components/ProductCard.tsx +0 -57
  148. package/dist/templates/storefront-additions/src/components/QuantitySelector.tsx +0 -35
  149. package/dist/templates/storefront-additions/src/context/CartContext.tsx +0 -158
  150. package/dist/templates/storefront-additions/src/lib/forgecart.ts +0 -22
  151. package/dist/templates/storefront-e2e-additions/playwright.config.ts +0 -26
  152. package/dist/templates/storefront-e2e-additions/src/example.spec.ts +0 -11
  153. package/dist/templates/workspace/.nxignore +0 -2
  154. package/templates/application-additions/.forgecartignore +0 -37
  155. package/templates/application-additions/Procfile +0 -1
  156. package/templates/application-additions/src/app/app.module.ts +0 -16
  157. package/templates/application-additions/src/services/forgecart.service.ts +0 -62
  158. package/templates/application-additions/src/services/index.ts +0 -2
  159. package/templates/application-additions/src/services/product-event-listener.service.ts +0 -150
  160. package/templates/application-e2e-additions/src/support/global-setup.ts +0 -16
  161. package/templates/application-e2e-additions/src/support/test-setup.ts +0 -9
  162. package/templates/storefront-additions/.forgecartignore +0 -64
  163. package/templates/storefront-additions/Procfile +0 -1
  164. package/templates/storefront-additions/package.json +0 -11
  165. package/templates/storefront-additions/postcss.config.js +0 -5
  166. package/templates/storefront-additions/src/app/cart/page.tsx +0 -77
  167. package/templates/storefront-additions/src/app/global.css +0 -1
  168. package/templates/storefront-additions/src/app/layout.tsx +0 -25
  169. package/templates/storefront-additions/src/app/page.tsx +0 -43
  170. package/templates/storefront-additions/src/components/AddToCartButton.tsx +0 -29
  171. package/templates/storefront-additions/src/components/CartItem.tsx +0 -82
  172. package/templates/storefront-additions/src/components/Header.tsx +0 -42
  173. package/templates/storefront-additions/src/components/ProductCard.tsx +0 -57
  174. package/templates/storefront-additions/src/components/QuantitySelector.tsx +0 -35
  175. package/templates/storefront-additions/src/context/CartContext.tsx +0 -158
  176. package/templates/storefront-additions/src/lib/forgecart.ts +0 -22
  177. package/templates/storefront-additions/tailwind.config.js +0 -8
  178. package/templates/storefront-e2e-additions/playwright.config.ts +0 -26
  179. package/templates/storefront-e2e-additions/src/example.spec.ts +0 -11
  180. package/templates/workspace/.nxignore +0 -2
@@ -0,0 +1,96 @@
1
+ import Link from 'next/link';
2
+ import { notFound } from 'next/navigation';
3
+
4
+ import { AddToCartButton } from '../../../components/AddToCartButton';
5
+ import { formatPrice, 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
+ const defaultVariant = product.variants[0] ?? null;
23
+
24
+ return (
25
+ <div className="space-y-6">
26
+ <Link href="/products" className="text-sm text-gray-600 hover:text-gray-900">
27
+ &larr; Back to products
28
+ </Link>
29
+
30
+ <div className="grid gap-8 md:grid-cols-2">
31
+ <div className="aspect-square overflow-hidden rounded-lg border border-gray-200 bg-gray-100">
32
+ {product.featuredAsset ? (
33
+ // eslint-disable-next-line @next/next/no-img-element
34
+ <img
35
+ src={product.featuredAsset.preview}
36
+ alt={product.name}
37
+ className="h-full w-full object-cover"
38
+ />
39
+ ) : (
40
+ <div className="flex h-full w-full items-center justify-center text-sm text-gray-400">
41
+ No image
42
+ </div>
43
+ )}
44
+ </div>
45
+
46
+ <div className="space-y-4">
47
+ <h1 className="text-2xl font-bold tracking-tight text-gray-900">
48
+ {product.name}
49
+ </h1>
50
+
51
+ {defaultVariant && (
52
+ <p className="text-xl font-semibold text-gray-900">
53
+ {formatPrice(defaultVariant.priceWithTax)}
54
+ </p>
55
+ )}
56
+
57
+ {product.description && (
58
+ <div
59
+ className="prose prose-sm max-w-none text-gray-600"
60
+ // Product descriptions may contain rich text from the dashboard.
61
+ dangerouslySetInnerHTML={{ __html: product.description }}
62
+ />
63
+ )}
64
+
65
+ {defaultVariant ? (
66
+ <AddToCartButton
67
+ line={{
68
+ variantId: defaultVariant.id,
69
+ productSlug: product.slug,
70
+ name: product.name,
71
+ unitPrice: defaultVariant.priceWithTax,
72
+ imageUrl: product.featuredAsset?.preview ?? null,
73
+ }}
74
+ />
75
+ ) : (
76
+ <p className="text-sm text-gray-500">Currently unavailable.</p>
77
+ )}
78
+
79
+ {product.variants.length > 1 && (
80
+ <div className="pt-2">
81
+ <h2 className="mb-2 text-sm font-medium text-gray-700">Variants</h2>
82
+ <ul className="space-y-1 text-sm text-gray-600">
83
+ {product.variants.map((variant) => (
84
+ <li key={variant.id} className="flex justify-between gap-4">
85
+ <span>{variant.name}</span>
86
+ <span>{formatPrice(variant.priceWithTax)}</span>
87
+ </li>
88
+ ))}
89
+ </ul>
90
+ </div>
91
+ )}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -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
+ }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ import { useCart, type CartLine } from '../lib/cart-context';
6
+
7
+ /**
8
+ * Adds a specific variant to the local cart. Rendered on the product detail
9
+ * page (a Server Component) with the variant's display data passed in as props.
10
+ */
11
+ export function AddToCartButton({ line }: { line: Omit<CartLine, 'quantity'> }) {
12
+ const { addLine } = useCart();
13
+ const [added, setAdded] = useState(false);
14
+
15
+ const onClick = () => {
16
+ addLine(line, 1);
17
+ setAdded(true);
18
+ window.setTimeout(() => setAdded(false), 1500);
19
+ };
20
+
21
+ return (
22
+ <button
23
+ type="button"
24
+ onClick={onClick}
25
+ className="rounded-md bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-700"
26
+ >
27
+ {added ? 'Added!' : 'Add to cart'}
28
+ </button>
29
+ );
30
+ }
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+
5
+ import { useCart } from '../lib/cart-context';
6
+
7
+ /** Top navigation: Home / Products / Cart, with a live cart item count. */
8
+ export function Header() {
9
+ const { itemCount } = useCart();
10
+
11
+ return (
12
+ <header className="border-b border-gray-200 bg-white">
13
+ <div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
14
+ <Link href="/" className="text-lg font-bold tracking-tight text-gray-900">
15
+ Storefront
16
+ </Link>
17
+ <nav className="flex items-center gap-6 text-sm font-medium text-gray-600">
18
+ <Link href="/" className="hover:text-gray-900">
19
+ Home
20
+ </Link>
21
+ <Link href="/products" className="hover:text-gray-900">
22
+ Products
23
+ </Link>
24
+ <Link href="/cart" className="relative hover:text-gray-900">
25
+ Cart
26
+ {itemCount > 0 && (
27
+ <span className="ml-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-gray-900 px-1.5 text-xs font-semibold text-white">
28
+ {itemCount}
29
+ </span>
30
+ )}
31
+ </Link>
32
+ </nav>
33
+ </div>
34
+ </header>
35
+ );
36
+ }
@@ -0,0 +1,43 @@
1
+ import Link from 'next/link';
2
+
3
+ import {
4
+ formatPrice,
5
+ getStartingPrice,
6
+ type StorefrontProduct,
7
+ } from '../lib/forgecart';
8
+
9
+ /** A product tile linking to its detail page. Used by the home and grid pages. */
10
+ export function ProductCard({ product }: { product: StorefrontProduct }) {
11
+ const startingPrice = getStartingPrice(product);
12
+
13
+ return (
14
+ <Link
15
+ href={`/products/${product.slug}`}
16
+ className="group flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white transition hover:shadow-md"
17
+ >
18
+ <div className="aspect-square overflow-hidden bg-gray-100">
19
+ {product.featuredAsset ? (
20
+ // Plain <img> keeps the template dependency-free of next/image config.
21
+ // eslint-disable-next-line @next/next/no-img-element
22
+ <img
23
+ src={product.featuredAsset.preview}
24
+ alt={product.name}
25
+ className="h-full w-full object-cover transition group-hover:scale-105"
26
+ />
27
+ ) : (
28
+ <div className="flex h-full w-full items-center justify-center text-sm text-gray-400">
29
+ No image
30
+ </div>
31
+ )}
32
+ </div>
33
+ <div className="flex flex-1 flex-col gap-1 p-4">
34
+ <h3 className="font-medium text-gray-900">{product.name}</h3>
35
+ {startingPrice !== null && (
36
+ <p className="text-sm text-gray-600">
37
+ From {formatPrice(startingPrice)}
38
+ </p>
39
+ )}
40
+ </div>
41
+ </Link>
42
+ );
43
+ }
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ type ReactNode,
11
+ } from 'react';
12
+
13
+ /**
14
+ * A line in the local cart. The MVP cart lives entirely in the browser
15
+ * (localStorage); wiring it to the ForgeCart `cart` API is left as a follow-up.
16
+ * `unitPrice` is in minor currency units (cents).
17
+ */
18
+ export interface CartLine {
19
+ variantId: string;
20
+ productSlug: string;
21
+ name: string;
22
+ unitPrice: number;
23
+ quantity: number;
24
+ imageUrl: string | null;
25
+ }
26
+
27
+ interface CartContextValue {
28
+ lines: CartLine[];
29
+ itemCount: number;
30
+ subtotal: number;
31
+ addLine: (line: Omit<CartLine, 'quantity'>, quantity?: number) => void;
32
+ setQuantity: (variantId: string, quantity: number) => void;
33
+ removeLine: (variantId: string) => void;
34
+ clear: () => void;
35
+ }
36
+
37
+ const STORAGE_KEY = 'forgecart.cart';
38
+
39
+ const CartContext = createContext<CartContextValue | null>(null);
40
+
41
+ function readStoredLines(): CartLine[] {
42
+ if (typeof window === 'undefined') {
43
+ return [];
44
+ }
45
+ try {
46
+ const raw = window.localStorage.getItem(STORAGE_KEY);
47
+ return raw ? (JSON.parse(raw) as CartLine[]) : [];
48
+ } catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ export function CartProvider({ children }: { children: ReactNode }) {
54
+ const [lines, setLines] = useState<CartLine[]>([]);
55
+
56
+ // Hydrate from localStorage once on mount (avoids SSR hydration mismatch).
57
+ useEffect(() => {
58
+ setLines(readStoredLines());
59
+ }, []);
60
+
61
+ // Persist on every change.
62
+ useEffect(() => {
63
+ if (typeof window === 'undefined') {
64
+ return;
65
+ }
66
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(lines));
67
+ }, [lines]);
68
+
69
+ const addLine = useCallback(
70
+ (line: Omit<CartLine, 'quantity'>, quantity = 1) => {
71
+ setLines((prev) => {
72
+ const existing = prev.find((l) => l.variantId === line.variantId);
73
+ if (existing) {
74
+ return prev.map((l) =>
75
+ l.variantId === line.variantId
76
+ ? { ...l, quantity: l.quantity + quantity }
77
+ : l,
78
+ );
79
+ }
80
+ return [...prev, { ...line, quantity }];
81
+ });
82
+ },
83
+ [],
84
+ );
85
+
86
+ const setQuantity = useCallback((variantId: string, quantity: number) => {
87
+ setLines((prev) =>
88
+ quantity <= 0
89
+ ? prev.filter((l) => l.variantId !== variantId)
90
+ : prev.map((l) => (l.variantId === variantId ? { ...l, quantity } : l)),
91
+ );
92
+ }, []);
93
+
94
+ const removeLine = useCallback((variantId: string) => {
95
+ setLines((prev) => prev.filter((l) => l.variantId !== variantId));
96
+ }, []);
97
+
98
+ const clear = useCallback(() => setLines([]), []);
99
+
100
+ const value = useMemo<CartContextValue>(() => {
101
+ const itemCount = lines.reduce((sum, l) => sum + l.quantity, 0);
102
+ const subtotal = lines.reduce((sum, l) => sum + l.unitPrice * l.quantity, 0);
103
+ return { lines, itemCount, subtotal, addLine, setQuantity, removeLine, clear };
104
+ }, [lines, addLine, setQuantity, removeLine, clear]);
105
+
106
+ return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
107
+ }
108
+
109
+ /** Access the cart. Must be used inside a `<CartProvider>`. */
110
+ export function useCart(): CartContextValue {
111
+ const ctx = useContext(CartContext);
112
+ if (!ctx) {
113
+ throw new Error('useCart must be used within a CartProvider');
114
+ }
115
+ return ctx;
116
+ }
@@ -0,0 +1,202 @@
1
+ import { ForgeCartShopClient } from '@forgecart/sdk';
2
+
3
+ /**
4
+ * Server-side ForgeCart shop client.
5
+ *
6
+ * The storefront talks to the ForgeCart shop GraphQL API on behalf of a single
7
+ * channel. The channel token is sent as the `forgecart-token` header on every
8
+ * request (the SDK adds it from `channelToken`), and the endpoint points at the
9
+ * channel's shop-api.
10
+ *
11
+ * Both values come from the environment (`.env.local`, written by
12
+ * `forgecart init`):
13
+ * - FORGECART_SHOP_API_URL -> endpoint
14
+ * - FORGECART_CHANNEL_TOKEN -> channelToken
15
+ *
16
+ * This module is server-only: it is imported exclusively from Server
17
+ * Components, so the channel token never reaches the browser. The construction
18
+ * mirrors the dashboard's `SdkClientService` (see
19
+ * app/dashboard/src/service/sdk-client.service.ts), adapted to read from env.
20
+ */
21
+
22
+ const SHOP_API_URL = process.env.FORGECART_SHOP_API_URL ?? '';
23
+ const CHANNEL_TOKEN = process.env.FORGECART_CHANNEL_TOKEN ?? '';
24
+
25
+ let client: ForgeCartShopClient | null = null;
26
+
27
+ /**
28
+ * Lazily construct and memoize the shop client.
29
+ *
30
+ * Constructing per-request would open a fresh websocket transport each time, so
31
+ * we keep a singleton for the lifetime of the server process.
32
+ */
33
+ export function getShopClient(): ForgeCartShopClient {
34
+ if (!SHOP_API_URL) {
35
+ throw new Error(
36
+ 'FORGECART_SHOP_API_URL is not set. Add it to .env.local (forgecart init writes it for you).',
37
+ );
38
+ }
39
+ if (!CHANNEL_TOKEN) {
40
+ throw new Error(
41
+ 'FORGECART_CHANNEL_TOKEN is not set. Add it to .env.local (forgecart init writes it for you).',
42
+ );
43
+ }
44
+ if (!client) {
45
+ client = new ForgeCartShopClient({
46
+ endpoint: SHOP_API_URL,
47
+ channelToken: CHANNEL_TOKEN,
48
+ });
49
+ }
50
+ return client;
51
+ }
52
+
53
+ /** Convenience singleton for direct use in Server Components. */
54
+ export const shopClient = (): ForgeCartShopClient => getShopClient();
55
+
56
+ /* ------------------------------------------------------------------ *
57
+ * Typed product helpers
58
+ *
59
+ * The generated typed operations (`shopClient.product.shopProducts` /
60
+ * `.shopProduct`) only select `id` + `optionGroups`, which is not enough to
61
+ * render a storefront. We therefore use the SDK's supported raw-query escape
62
+ * hatch (`ForgeCartShopClient.query<T>(document, variables)`) with documents
63
+ * that select the display fields. Every field below exists on the shop schema's
64
+ * `Product` / `ProductVariant` / `Asset` / `ProductList` types (available via
65
+ * `@forgecart/sdk/shop`).
66
+ * ------------------------------------------------------------------ */
67
+
68
+ /** A single purchasable variant of a product. Prices are minor units (cents). */
69
+ export interface StorefrontVariant {
70
+ id: string;
71
+ name: string;
72
+ sku: string | null;
73
+ /** Price in minor currency units (e.g. cents). */
74
+ price: number;
75
+ /** Price including tax, in minor currency units. */
76
+ priceWithTax: number;
77
+ }
78
+
79
+ /** A product as the storefront renders it. */
80
+ export interface StorefrontProduct {
81
+ id: string;
82
+ name: string;
83
+ slug: string;
84
+ description: string | null;
85
+ /** Preview URL of the featured image, if any. */
86
+ featuredAsset: { id: string; preview: string } | null;
87
+ variants: StorefrontVariant[];
88
+ }
89
+
90
+ /** A page of products plus the total count for pagination. */
91
+ export interface StorefrontProductList {
92
+ items: StorefrontProduct[];
93
+ totalItems: number;
94
+ }
95
+
96
+ const PRODUCT_FIELDS = `
97
+ id
98
+ name
99
+ slug
100
+ description
101
+ featuredAsset {
102
+ id
103
+ preview
104
+ }
105
+ variants {
106
+ id
107
+ name
108
+ sku
109
+ price
110
+ priceWithTax
111
+ }
112
+ `;
113
+
114
+ const PRODUCTS_QUERY = `query StorefrontProducts($options: ListQueryOptions) {
115
+ products(options: $options) {
116
+ items {${PRODUCT_FIELDS}}
117
+ totalItems
118
+ }
119
+ }`;
120
+
121
+ const PRODUCT_BY_SLUG_QUERY = `query StorefrontProduct($slug: String) {
122
+ product(slug: $slug) {${PRODUCT_FIELDS}}
123
+ }`;
124
+
125
+ const PRODUCT_BY_ID_QUERY = `query StorefrontProductById($id: ID) {
126
+ product(id: $id) {${PRODUCT_FIELDS}}
127
+ }`;
128
+
129
+ interface ProductsQueryResult {
130
+ products: {
131
+ items: StorefrontProduct[];
132
+ totalItems: number;
133
+ };
134
+ }
135
+
136
+ interface ProductQueryResult {
137
+ product: StorefrontProduct | null;
138
+ }
139
+
140
+ /**
141
+ * Fetch a page of products for the channel.
142
+ *
143
+ * `take` / `skip` map onto the shop API's `ListQueryOptions`.
144
+ */
145
+ export async function getProducts(
146
+ options: { take?: number; skip?: number } = {},
147
+ ): Promise<StorefrontProductList> {
148
+ const { take = 24, skip = 0 } = options;
149
+ const result = await getShopClient().query<ProductsQueryResult>(PRODUCTS_QUERY, {
150
+ options: { take, skip },
151
+ });
152
+ return {
153
+ items: result.products.items,
154
+ totalItems: result.products.totalItems,
155
+ };
156
+ }
157
+
158
+ /** Fetch a single product by its URL slug. Returns `null` if not found. */
159
+ export async function getProductBySlug(slug: string): Promise<StorefrontProduct | null> {
160
+ const result = await getShopClient().query<ProductQueryResult>(PRODUCT_BY_SLUG_QUERY, {
161
+ slug,
162
+ });
163
+ return result.product ?? null;
164
+ }
165
+
166
+ /** Fetch a single product by id. Returns `null` if not found. */
167
+ export async function getProductById(id: string): Promise<StorefrontProduct | null> {
168
+ const result = await getShopClient().query<ProductQueryResult>(PRODUCT_BY_ID_QUERY, {
169
+ id,
170
+ });
171
+ return result.product ?? null;
172
+ }
173
+
174
+ /**
175
+ * Convenience: fetch a small set of products to feature on the home page.
176
+ */
177
+ export async function getFeaturedProducts(count = 4): Promise<StorefrontProduct[]> {
178
+ const { items } = await getProducts({ take: count });
179
+ return items;
180
+ }
181
+
182
+ /** Lowest variant price for a product, in minor units, or `null` if none. */
183
+ export function getStartingPrice(product: StorefrontProduct): number | null {
184
+ if (product.variants.length === 0) {
185
+ return null;
186
+ }
187
+ return product.variants.reduce(
188
+ (min, v) => (v.priceWithTax < min ? v.priceWithTax : min),
189
+ product.variants[0].priceWithTax,
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Format a minor-unit price (cents) as a currency string. Defaults to USD;
195
+ * pass a different ISO currency code as needed.
196
+ */
197
+ export function formatPrice(minorUnits: number, currency = 'USD'): string {
198
+ return new Intl.NumberFormat('en-US', {
199
+ style: 'currency',
200
+ currency,
201
+ }).format(minorUnits / 100);
202
+ }
@@ -5,4 +5,4 @@ module.exports = {
5
5
  extend: {},
6
6
  },
7
7
  plugins: [],
8
- }
8
+ };
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": {
18
+ "@/*": ["./src/*"]
19
+ }
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2022 Vendure
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.