@vitrine-kit/vitrine 0.3.0 → 0.4.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/README.md +18 -18
- package/dist/index.js +304 -285
- package/kit/registry/README.md +9 -0
- package/kit/registry/_index.json +74 -0
- package/kit/registry/cart/docs/cart.md +13 -0
- package/kit/registry/cart/feature.json +51 -0
- package/kit/registry/cart/files/app/(frontend)/cart/page.tsx +21 -0
- package/kit/registry/cart/files/app/api/cart/route.ts +41 -0
- package/kit/registry/cart/files/components/cart/AddToCart.tsx +40 -0
- package/kit/registry/cart/files/components/cart/CartIndicator.tsx +11 -0
- package/kit/registry/cart/files/components/cart/CartLineItem.tsx +27 -0
- package/kit/registry/cart/files/components/cart/CartSummary.tsx +31 -0
- package/kit/registry/cart/files/components/cart/CartView.tsx +31 -0
- package/kit/registry/cart/files/lib/cart/data.ts +12 -0
- package/kit/registry/cart/files/lib/cart/register.ts +10 -0
- package/kit/registry/catalog/docs/catalog.md +18 -0
- package/kit/registry/catalog/feature.json +43 -0
- package/kit/registry/catalog/files/components/catalog/CategoryNav.tsx +26 -0
- package/kit/registry/catalog/files/components/catalog/ProductCard.tsx +42 -0
- package/kit/registry/catalog/files/components/catalog/ProductGrid.tsx +25 -0
- package/kit/registry/catalog/files/lib/catalog/data.ts +24 -0
- package/kit/registry/catalog/files/lib/catalog/register.ts +8 -0
- package/kit/registry/checkout/docs/checkout.md +17 -0
- package/kit/registry/checkout/feature.json +47 -0
- package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -0
- package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +31 -0
- package/kit/registry/checkout/files/lib/checkout/fulfill.ts +72 -0
- package/kit/registry/checkout/files/lib/checkout/register.ts +8 -0
- package/kit/registry/checkout-paddle/docs/checkout-paddle.md +22 -0
- package/kit/registry/checkout-paddle/feature.json +62 -0
- package/kit/registry/checkout-paddle/files/app/api/webhooks/paddle/route.ts +22 -0
- package/kit/registry/checkout-paddle/files/lib/checkout-paddle/provider.ts +59 -0
- package/kit/registry/checkout-paddle/files/lib/checkout-paddle/register.ts +7 -0
- package/kit/registry/checkout-stripe/docs/checkout-stripe.md +17 -0
- package/kit/registry/checkout-stripe/feature.json +54 -0
- package/kit/registry/checkout-stripe/files/app/api/webhooks/stripe/route.ts +23 -0
- package/kit/registry/checkout-stripe/files/lib/checkout-stripe/provider.ts +73 -0
- package/kit/registry/checkout-stripe/files/lib/checkout-stripe/register.ts +7 -0
- package/kit/registry/checkout-yookassa/docs/checkout-yookassa.md +20 -0
- package/kit/registry/checkout-yookassa/feature.json +52 -0
- package/kit/registry/checkout-yookassa/files/app/api/webhooks/yookassa/route.ts +22 -0
- package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/provider.ts +82 -0
- package/kit/registry/checkout-yookassa/files/lib/checkout-yookassa/register.ts +7 -0
- package/kit/registry/product-page/docs/product-page.md +16 -0
- package/kit/registry/product-page/feature.json +39 -0
- package/kit/registry/product-page/files/components/product/ProductGallery.tsx +37 -0
- package/kit/registry/product-page/files/components/product/ProductView.tsx +42 -0
- package/kit/registry/product-page/files/lib/product/data.ts +10 -0
- package/kit/registry/seo/docs/seo.md +14 -0
- package/kit/registry/seo/feature.json +35 -0
- package/kit/registry/seo/files/components/seo/JsonLd.tsx +15 -0
- package/kit/registry/seo/files/lib/seo/jsonld.ts +29 -0
- package/kit/registry/seo/files/lib/seo/metadata.ts +35 -0
- package/kit/registry/stubs/route-glue.d.ts +45 -0
- package/kit/registry/tsconfig.json +21 -0
- package/kit/registry/tsconfig.routes.json +9 -0
- package/kit/templates/README.md +27 -0
- package/kit/templates/backend-payload/files/.dockerignore +8 -0
- package/kit/templates/backend-payload/files/Dockerfile +29 -0
- package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/not-found.tsx +18 -0
- package/kit/templates/backend-payload/files/app/(payload)/admin/[[...segments]]/page.tsx +18 -0
- package/kit/templates/backend-payload/files/app/(payload)/admin/importMap.js +3 -0
- package/kit/templates/backend-payload/files/app/(payload)/api/[...slug]/route.ts +18 -0
- package/kit/templates/backend-payload/files/app/(payload)/api/graphql/route.ts +6 -0
- package/kit/templates/backend-payload/files/app/(payload)/api/graphql-playground/route.ts +5 -0
- package/kit/templates/backend-payload/files/app/(payload)/custom.scss +1 -0
- package/kit/templates/backend-payload/files/app/(payload)/layout.tsx +23 -0
- package/kit/templates/backend-payload/files/docker-compose.yml +35 -0
- package/kit/templates/backend-payload/files/lib/adapter/db-decision.ts +43 -0
- package/kit/templates/backend-payload/files/lib/adapter/db-ping.ts +29 -0
- package/kit/templates/backend-payload/files/lib/adapter/db.ts +27 -0
- package/kit/templates/backend-payload/files/lib/adapter/index.ts +31 -0
- package/kit/templates/backend-payload/files/lib/adapter/map.ts +112 -0
- package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +79 -0
- package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +154 -0
- package/kit/templates/backend-payload/files/lib/adapter/payload-types.ts +50 -0
- package/kit/templates/backend-payload/files/lib/seed/admin.ts +23 -0
- package/kit/templates/backend-payload/files/lib/seed/demo.ts +79 -0
- package/kit/templates/backend-payload/files/lib/seed/guards.ts +5 -0
- package/kit/templates/backend-payload/files/lib/seed/richtext.ts +26 -0
- package/kit/templates/backend-payload/files/lib/seed/run.ts +51 -0
- package/kit/templates/backend-payload/files/next.config.mjs +10 -0
- package/kit/templates/backend-payload/files/payload.config.ts +32 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-1.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-2.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-3.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-4.svg +5 -0
- package/kit/templates/backend-payload/files/seed-assets/placeholder-5.svg +5 -0
- package/kit/templates/backend-vendure/files/.dockerignore +8 -0
- package/kit/templates/backend-vendure/files/Dockerfile +24 -0
- package/kit/templates/backend-vendure/files/docker-compose.yml +50 -0
- package/kit/templates/backend-vendure/files/lib/adapter/graphql.ts +27 -0
- package/kit/templates/backend-vendure/files/lib/adapter/index.ts +20 -0
- package/kit/templates/backend-vendure/files/lib/adapter/map.ts +107 -0
- package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +62 -0
- package/kit/templates/backend-vendure/files/lib/adapter/vendure-commerce.ts +76 -0
- package/kit/templates/backend-vendure/files/lib/adapter/vendure-types.ts +61 -0
- package/kit/templates/backend-vendure/files/lib/seed/populate.ts +22 -0
- package/kit/templates/backend-vendure/files/src/index.ts +15 -0
- package/kit/templates/backend-vendure/files/vendure-config.ts +56 -0
- package/kit/templates/base/files/.claude/commands/add-feature.md +18 -0
- package/kit/templates/base/files/.claude/commands/design.md +14 -0
- package/kit/templates/base/files/.claude/commands/doctor.md +15 -0
- package/kit/templates/base/files/.claude/commands/setup.md +15 -0
- package/kit/templates/base/files/.claude/commands/update.md +16 -0
- package/kit/templates/base/files/AGENTS.md +20 -0
- package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +28 -0
- package/kit/templates/base/files/app/(frontend)/layout.tsx +32 -0
- package/kit/templates/base/files/app/(frontend)/page.tsx +27 -0
- package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +43 -0
- package/kit/templates/base/files/app/globals.css +6 -0
- package/kit/templates/base/files/components/layout/SiteFooter.tsx +15 -0
- package/kit/templates/base/files/components/layout/SiteHeader.tsx +23 -0
- package/kit/templates/base/files/design/README.md +9 -0
- package/kit/templates/base/files/lib/site.ts +10 -0
- package/kit/templates/base/files/next.config.mjs +7 -0
- package/kit/templates/base/files/postcss.config.mjs +7 -0
- package/kit/templates/base/files/public/.gitkeep +0 -0
- package/kit/templates/base/files/tailwind.config.ts +13 -0
- package/kit/templates/base/files/tsconfig.json +25 -0
- package/kit/templates/tsconfig.json +18 -0
- package/package.json +7 -6
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Feature: product-page
|
|
2
|
+
|
|
3
|
+
The product detail page. Depends on `catalog` (registryDependencies).
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
- `components/product/ProductView` — the detail card. **Hosts the slots** `product.gallery`, `product.below-title`, `product.below-price`, `product.purchase`, `product.below-description`, `product.tabs` — other features (reviews, cart, wishlist) mount here.
|
|
7
|
+
- `components/product/ProductGallery` — the image gallery.
|
|
8
|
+
|
|
9
|
+
## Data
|
|
10
|
+
- `lib/product/data` — `loadProduct(source, slug)` over `CatalogSource.getProduct`.
|
|
11
|
+
|
|
12
|
+
## Slots
|
|
13
|
+
The feature doesn't register slots, it **provides** them: so adding reviews/cart doesn't require editing `ProductView`.
|
|
14
|
+
|
|
15
|
+
## Contracts
|
|
16
|
+
Only `@vitrine-kit/contracts` (types) and `@vitrine-kit/core/react` (`<Slot>`).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schemas/feature.schema.json",
|
|
3
|
+
"name": "product-page",
|
|
4
|
+
"title": "Product page",
|
|
5
|
+
"kitVersion": "0.4.0",
|
|
6
|
+
"requiresContracts": ">=1.0.0 <2.0.0",
|
|
7
|
+
"tier": [
|
|
8
|
+
"catalog",
|
|
9
|
+
"simple-store",
|
|
10
|
+
"full-store"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"catalog"
|
|
14
|
+
],
|
|
15
|
+
"corePackages": {
|
|
16
|
+
"@vitrine-kit/core": ">=0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"npm": [],
|
|
19
|
+
"files": [
|
|
20
|
+
{
|
|
21
|
+
"from": "files/components/product/",
|
|
22
|
+
"to": "components/product/"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"from": "files/lib/product/",
|
|
26
|
+
"to": "lib/product/"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"config": {
|
|
30
|
+
"set": {
|
|
31
|
+
"features.product-page": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"slots": [],
|
|
35
|
+
"env": [],
|
|
36
|
+
"claudeDoc": "docs/product-page.md",
|
|
37
|
+
"conflicts": [],
|
|
38
|
+
"removable": false
|
|
39
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Product gallery — presentational, neutral.
|
|
2
|
+
import type { ProductImage } from '@vitrine-kit/contracts';
|
|
3
|
+
|
|
4
|
+
export interface ProductGalleryProps {
|
|
5
|
+
images: ProductImage[];
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ProductGallery({ images, title }: ProductGalleryProps) {
|
|
10
|
+
const cover = images[0];
|
|
11
|
+
if (!cover) {
|
|
12
|
+
return <div className="aspect-square w-full rounded-md bg-muted" aria-hidden="true" />;
|
|
13
|
+
}
|
|
14
|
+
const rest = images.slice(1);
|
|
15
|
+
return (
|
|
16
|
+
<div className="vt-product-gallery">
|
|
17
|
+
<img
|
|
18
|
+
src={cover.url}
|
|
19
|
+
alt={cover.alt ?? title}
|
|
20
|
+
className="w-full rounded-md object-cover"
|
|
21
|
+
/>
|
|
22
|
+
{rest.length > 0 ? (
|
|
23
|
+
<ul role="list" className="mt-gutter flex gap-gutter">
|
|
24
|
+
{rest.map((img, i) => (
|
|
25
|
+
<li key={img.url}>
|
|
26
|
+
<img
|
|
27
|
+
src={img.url}
|
|
28
|
+
alt={img.alt ?? `${title} — image ${i + 2}`}
|
|
29
|
+
className="h-16 w-16 rounded-md object-cover"
|
|
30
|
+
/>
|
|
31
|
+
</li>
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
) : null}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Product detail card. HOSTS the product.* slots — other features (reviews,
|
|
2
|
+
// cart, wishlist) mount into them without editing this component.
|
|
3
|
+
import type { Product } from '@vitrine-kit/contracts';
|
|
4
|
+
import { Slot } from '@vitrine-kit/core/react';
|
|
5
|
+
import { ProductGallery } from './ProductGallery.js';
|
|
6
|
+
import { formatPrice } from '../../lib/product/data.js';
|
|
7
|
+
|
|
8
|
+
export interface ProductViewProps {
|
|
9
|
+
product: Product;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProductView({ product }: ProductViewProps) {
|
|
13
|
+
const price = product.priceRange?.min ?? product.variants[0]?.price;
|
|
14
|
+
const currency = product.priceRange?.currency ?? product.variants[0]?.currency ?? 'USD';
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<article className="vt-product-view grid gap-section md:grid-cols-2">
|
|
18
|
+
<div>
|
|
19
|
+
<Slot
|
|
20
|
+
name="product.gallery"
|
|
21
|
+
fallback={<ProductGallery images={product.images} title={product.title} />}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="flex flex-col gap-gutter">
|
|
25
|
+
<Slot name="product.below-title" />
|
|
26
|
+
<h1 className="font-heading text-fg">{product.title}</h1>
|
|
27
|
+
{price != null ? (
|
|
28
|
+
<p className="text-price text-xl">{formatPrice(price, currency)}</p>
|
|
29
|
+
) : null}
|
|
30
|
+
<Slot name="product.below-price" />
|
|
31
|
+
<div className="vt-product-purchase">
|
|
32
|
+
<Slot name="product.purchase" product={product} />
|
|
33
|
+
</div>
|
|
34
|
+
{product.description ? (
|
|
35
|
+
<div className="vt-product-description text-fg">{product.description}</div>
|
|
36
|
+
) : null}
|
|
37
|
+
<Slot name="product.below-description" />
|
|
38
|
+
<Slot name="product.tabs" />
|
|
39
|
+
</div>
|
|
40
|
+
</article>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CatalogSource, Product } from '@vitrine-kit/contracts';
|
|
2
|
+
|
|
3
|
+
export async function loadProduct(source: CatalogSource, slug: string): Promise<Product | null> {
|
|
4
|
+
return source.getProduct(slug);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Money — minor units (e.g. cents). */
|
|
8
|
+
export function formatPrice(amount: number, currency: string, locale = 'en-US'): string {
|
|
9
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount / 100);
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Feature: seo
|
|
2
|
+
|
|
3
|
+
Page metadata and schema.org JSON-LD from domain types.
|
|
4
|
+
|
|
5
|
+
## Modules
|
|
6
|
+
- `lib/seo/metadata` — `buildProductMetadata(product)` → `SiteMetadata` (structurally compatible with Next `Metadata`; in the client it's returned from `generateMetadata`).
|
|
7
|
+
- `lib/seo/jsonld` — `productJsonLd(product)` → a schema.org `Product`/`Offer` object.
|
|
8
|
+
- `components/seo/JsonLd` — `<JsonLd data={…} />` embeds a `<script type="application/ld+json">`.
|
|
9
|
+
|
|
10
|
+
## Contracts
|
|
11
|
+
Only `@vitrine-kit/contracts` (the `Product` type). Registers no slots.
|
|
12
|
+
|
|
13
|
+
## Note
|
|
14
|
+
The price in JSON-LD is converted from minor units (`price/100`).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schemas/feature.schema.json",
|
|
3
|
+
"name": "seo",
|
|
4
|
+
"title": "SEO — metadata and JSON-LD",
|
|
5
|
+
"kitVersion": "0.4.0",
|
|
6
|
+
"requiresContracts": ">=1.0.0 <2.0.0",
|
|
7
|
+
"tier": [
|
|
8
|
+
"catalog",
|
|
9
|
+
"simple-store",
|
|
10
|
+
"full-store"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [],
|
|
13
|
+
"corePackages": {},
|
|
14
|
+
"npm": [],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"from": "files/components/seo/",
|
|
18
|
+
"to": "components/seo/"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"from": "files/lib/seo/",
|
|
22
|
+
"to": "lib/seo/"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"config": {
|
|
26
|
+
"set": {
|
|
27
|
+
"features.seo": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"slots": [],
|
|
31
|
+
"env": [],
|
|
32
|
+
"claudeDoc": "docs/seo.md",
|
|
33
|
+
"conflicts": [],
|
|
34
|
+
"removable": false
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Embeds JSON-LD into the page. Usage:
|
|
2
|
+
// <JsonLd data={productJsonLd(product, { baseUrl })} />
|
|
3
|
+
export interface JsonLdProps {
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function JsonLd({ data }: JsonLdProps) {
|
|
8
|
+
return (
|
|
9
|
+
<script
|
|
10
|
+
type="application/ld+json"
|
|
11
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD is safe — serialized data, not user HTML.
|
|
12
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// schema.org JSON-LD from domain types.
|
|
2
|
+
import type { Product } from '@vitrine-kit/contracts';
|
|
3
|
+
|
|
4
|
+
export function productJsonLd(
|
|
5
|
+
product: Product,
|
|
6
|
+
opts: { baseUrl?: string } = {},
|
|
7
|
+
): Record<string, unknown> {
|
|
8
|
+
const variant = product.variants[0];
|
|
9
|
+
const price = product.priceRange?.min ?? variant?.price;
|
|
10
|
+
const currency = product.priceRange?.currency ?? variant?.currency;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
'@context': 'https://schema.org',
|
|
14
|
+
'@type': 'Product',
|
|
15
|
+
name: product.title,
|
|
16
|
+
description: product.seo?.description ?? product.description,
|
|
17
|
+
image: product.images.map((image) => image.url),
|
|
18
|
+
sku: variant?.sku,
|
|
19
|
+
offers:
|
|
20
|
+
price != null
|
|
21
|
+
? {
|
|
22
|
+
'@type': 'Offer',
|
|
23
|
+
price: (price / 100).toFixed(2),
|
|
24
|
+
priceCurrency: currency,
|
|
25
|
+
url: opts.baseUrl ? `${opts.baseUrl}/products/${product.slug}` : undefined,
|
|
26
|
+
}
|
|
27
|
+
: undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Builds page metadata from domain types. SiteMetadata is structurally
|
|
2
|
+
// compatible with Next Metadata — in the client, generateMetadata() returns this.
|
|
3
|
+
import type { Product } from '@vitrine-kit/contracts';
|
|
4
|
+
|
|
5
|
+
export interface SiteMetadata {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
canonical?: string;
|
|
9
|
+
openGraph?: {
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
images?: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildProductMetadata(
|
|
17
|
+
product: Product,
|
|
18
|
+
opts: { baseUrl?: string } = {},
|
|
19
|
+
): SiteMetadata {
|
|
20
|
+
const title = product.seo?.title ?? product.title;
|
|
21
|
+
const description = product.seo?.description ?? product.description;
|
|
22
|
+
const image = product.seo?.image ?? product.images[0]?.url;
|
|
23
|
+
const canonical = opts.baseUrl ? `${opts.baseUrl}/products/${product.slug}` : undefined;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
canonical,
|
|
29
|
+
openGraph: {
|
|
30
|
+
title,
|
|
31
|
+
description,
|
|
32
|
+
images: image ? [image] : undefined,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Ambient stubs so registry API route glue typechecks in the monorepo without Next/Payload installed.
|
|
2
|
+
// Real stack types are verified when the client is instantiated.
|
|
3
|
+
|
|
4
|
+
declare module 'next/server' {
|
|
5
|
+
export class NextResponse {
|
|
6
|
+
static json(body: unknown, init?: ResponseInit): NextResponse;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module 'next/headers' {
|
|
11
|
+
interface CookieStore {
|
|
12
|
+
get(name: string): { value: string } | undefined;
|
|
13
|
+
set(name: string, value: string, options?: Record<string, unknown>): void;
|
|
14
|
+
}
|
|
15
|
+
export function cookies(): Promise<CookieStore>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare module '@/lib/adapter' {
|
|
19
|
+
import type { CatalogSource, CommerceBackend } from '@vitrine-kit/contracts';
|
|
20
|
+
export function getCatalogSource(): Promise<CatalogSource>;
|
|
21
|
+
export function getCommerceBackend(): Promise<CommerceBackend>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module '@/lib/checkout/fulfill' {
|
|
25
|
+
import type { NormalizedPaymentEvent, PaymentProviderName } from '@vitrine-kit/core';
|
|
26
|
+
export function fulfillOrderFromEvent(
|
|
27
|
+
event: NormalizedPaymentEvent,
|
|
28
|
+
providerName: PaymentProviderName,
|
|
29
|
+
): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare module '@/lib/checkout-stripe/provider' {
|
|
33
|
+
import type { PaymentProvider } from '@vitrine-kit/core';
|
|
34
|
+
export const stripeProvider: PaymentProvider;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare module '@/lib/checkout-paddle/provider' {
|
|
38
|
+
import type { PaymentProvider } from '@vitrine-kit/core';
|
|
39
|
+
export const paddleProvider: PaymentProvider;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module '@/lib/checkout-yookassa/provider' {
|
|
43
|
+
import type { PaymentProvider } from '@vitrine-kit/core';
|
|
44
|
+
export const yookassaProvider: PaymentProvider;
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"types": ["react", "node"]
|
|
7
|
+
},
|
|
8
|
+
"//": "We typecheck the presentational components/ + the contract-based lib/ (they depend only on contracts/core). Next glue under files/app/** (routes, server actions, webhook) and payment glue (provider.ts pulls in the provider SDK, fulfill.ts — payload/@payload-config) are checked when the client is instantiated, not in the kit.",
|
|
9
|
+
"include": [
|
|
10
|
+
"*/files/components/**/*.ts",
|
|
11
|
+
"*/files/components/**/*.tsx",
|
|
12
|
+
"*/files/lib/**/*.ts",
|
|
13
|
+
"*/files/lib/**/*.tsx"
|
|
14
|
+
],
|
|
15
|
+
"exclude": [
|
|
16
|
+
"**/node_modules",
|
|
17
|
+
"**/dist",
|
|
18
|
+
"*/files/lib/checkout-*/**",
|
|
19
|
+
"*/files/lib/checkout/fulfill.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"types": ["node"]
|
|
6
|
+
},
|
|
7
|
+
"//": "Next API route glue (cart/checkout/webhooks). Uses registry/stubs/route-glue.d.ts instead of installing Next/Payload in the kit repo.",
|
|
8
|
+
"include": ["stubs/**/*.d.ts", "*/files/app/api/**/route.ts"]
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# templates/ — client repository skeletons
|
|
2
|
+
|
|
3
|
+
`vitrine init` assembles the client repository from these (spec §6). `templates/<name>/files`
|
|
4
|
+
**mirrors the client root** (like `registry/<feature>/files`): the static skeleton is copied as-is,
|
|
5
|
+
then the CLI generates managed files on top of it (site.config.ts, vitrine.json, CLAUDE.md,
|
|
6
|
+
package.json, lib/slots.ts, lib/payments.ts, lib/blueprint.ts, theme/client.css).
|
|
7
|
+
|
|
8
|
+
- `base` — Next.js (App Router, route group `(frontend)`) + Tailwind (Vitrine preset),
|
|
9
|
+
storefront routes (home/catalog, product, category), header/footer hosting slots,
|
|
10
|
+
`.gitignore` (with `.vitrine/`), `/design` README.
|
|
11
|
+
- `backend-payload` — Payload config, the `(payload)` admin, a `PayloadCatalogSource`
|
|
12
|
+
adapter over the `CatalogSource` contract, zero-config dev
|
|
13
|
+
(SQLite fallback + demo seed + dev admin, §18), `Dockerfile` + `docker-compose.yml`
|
|
14
|
+
(app + Postgres, VPS hosting target).
|
|
15
|
+
- `backend-vendure` — A Vendure server (`vendure-config.ts`, Postgres/SQLite-dev db,
|
|
16
|
+
superadmin from env), `VendureCatalogSource`/`VendureCommerceBackend` adapters over the Shop
|
|
17
|
+
GraphQL API, a populate guard (§18 equivalent), `Dockerfile` + `docker-compose.yml` (db + server + web).
|
|
18
|
+
The storefront (`app/(frontend)`) and the catalog/cart features are the same as on Payload
|
|
19
|
+
(portability via contracts). Full payments use the Vendure Stripe plugin; the GPL-3.0 legal review
|
|
20
|
+
is a separate track.
|
|
21
|
+
|
|
22
|
+
## What is checked in the monorepo
|
|
23
|
+
|
|
24
|
+
Template files that depend on Next/Payload are typechecked **when the client is instantiated**
|
|
25
|
+
(the stack is installed there). The pure critical logic (Payload→contract mappers, the §18.1 DB
|
|
26
|
+
selection table, demo-seed invariants, dev-procedure guards) depends only on the contracts and is
|
|
27
|
+
covered by `pnpm typecheck:templates` + tests in `sandbox/`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
# Reference Vitrine client image (Next standalone + Payload). Target — VPS.
|
|
3
|
+
# @vitrine-kit/* packages are public on npm — no token needed to install.
|
|
4
|
+
FROM node:20-alpine AS base
|
|
5
|
+
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# --- dependencies ----------------------------------------------------------
|
|
9
|
+
FROM base AS deps
|
|
10
|
+
COPY package.json pnpm-lock.yaml* ./
|
|
11
|
+
RUN pnpm install --frozen-lockfile || pnpm install
|
|
12
|
+
|
|
13
|
+
# --- build ------------------------------------------------------------------
|
|
14
|
+
FROM base AS build
|
|
15
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
16
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
17
|
+
COPY . .
|
|
18
|
+
RUN pnpm build
|
|
19
|
+
|
|
20
|
+
# --- runtime ----------------------------------------------------------------
|
|
21
|
+
FROM base AS runtime
|
|
22
|
+
ENV NODE_ENV=production
|
|
23
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
COPY --from=build /app/public ./public
|
|
26
|
+
COPY --from=build /app/.next/standalone ./
|
|
27
|
+
COPY --from=build /app/.next/static ./.next/static
|
|
28
|
+
EXPOSE 3000
|
|
29
|
+
CMD ["node", "server.js"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Equivalent of the Payload-generated admin 404 page. */
|
|
2
|
+
import type { Metadata } from 'next';
|
|
3
|
+
import config from '@payload-config';
|
|
4
|
+
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views';
|
|
5
|
+
import { importMap } from '../importMap.js';
|
|
6
|
+
|
|
7
|
+
type Args = {
|
|
8
|
+
params: Promise<{ segments: string[] }>;
|
|
9
|
+
searchParams: Promise<{ [key: string]: string | string[] }>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
|
13
|
+
generatePageMetadata({ config, params, searchParams });
|
|
14
|
+
|
|
15
|
+
const NotFound = ({ params, searchParams }: Args) =>
|
|
16
|
+
NotFoundPage({ config, params, searchParams, importMap });
|
|
17
|
+
|
|
18
|
+
export default NotFound;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Equivalent of the Payload-generated admin page. */
|
|
2
|
+
import type { Metadata } from 'next';
|
|
3
|
+
import config from '@payload-config';
|
|
4
|
+
import { generatePageMetadata, RootPage } from '@payloadcms/next/views';
|
|
5
|
+
import { importMap } from '../importMap.js';
|
|
6
|
+
|
|
7
|
+
type Args = {
|
|
8
|
+
params: Promise<{ segments: string[] }>;
|
|
9
|
+
searchParams: Promise<{ [key: string]: string | string[] }>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
|
13
|
+
generatePageMetadata({ config, params, searchParams });
|
|
14
|
+
|
|
15
|
+
const Page = ({ params, searchParams }: Args) =>
|
|
16
|
+
RootPage({ config, params, searchParams, importMap });
|
|
17
|
+
|
|
18
|
+
export default Page;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Payload REST API (equivalent of the generated file). */
|
|
2
|
+
import config from '@payload-config';
|
|
3
|
+
import '@payloadcms/next/css';
|
|
4
|
+
import {
|
|
5
|
+
REST_DELETE,
|
|
6
|
+
REST_GET,
|
|
7
|
+
REST_OPTIONS,
|
|
8
|
+
REST_PATCH,
|
|
9
|
+
REST_POST,
|
|
10
|
+
REST_PUT,
|
|
11
|
+
} from '@payloadcms/next/routes';
|
|
12
|
+
|
|
13
|
+
export const GET = REST_GET(config);
|
|
14
|
+
export const POST = REST_POST(config);
|
|
15
|
+
export const DELETE = REST_DELETE(config);
|
|
16
|
+
export const PATCH = REST_PATCH(config);
|
|
17
|
+
export const PUT = REST_PUT(config);
|
|
18
|
+
export const OPTIONS = REST_OPTIONS(config);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* Payload GraphQL API (equivalent of the generated file). */
|
|
2
|
+
import config from '@payload-config';
|
|
3
|
+
import { GRAPHQL_POST, GRAPHQL_OPTIONS } from '@payloadcms/next/routes';
|
|
4
|
+
|
|
5
|
+
export const POST = GRAPHQL_POST(config);
|
|
6
|
+
export const OPTIONS = GRAPHQL_OPTIONS(config);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Custom Payload admin styles (optional). */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* Payload admin layout (route group (payload)). Equivalent of the Payload-generated
|
|
2
|
+
file — renders its own <html> separately from the storefront ((frontend)). */
|
|
3
|
+
import type { ServerFunctionClient } from 'payload';
|
|
4
|
+
import config from '@payload-config';
|
|
5
|
+
import '@payloadcms/next/css';
|
|
6
|
+
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts';
|
|
7
|
+
import type React from 'react';
|
|
8
|
+
import { importMap } from './admin/importMap.js';
|
|
9
|
+
import './custom.scss';
|
|
10
|
+
|
|
11
|
+
// biome-ignore lint/complexity/useArrowFunction: mirrors the Payload-generated file ('use server' boundary).
|
|
12
|
+
const serverFunction: ServerFunctionClient = async function (args) {
|
|
13
|
+
'use server';
|
|
14
|
+
return handleServerFunctions({ ...args, config, importMap });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Layout = ({ children }: { children: React.ReactNode }) => (
|
|
18
|
+
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
|
19
|
+
{children}
|
|
20
|
+
</RootLayout>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export default Layout;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Reference Vitrine deployment on a VPS: app (Next+Payload) + Postgres.
|
|
2
|
+
# Run: PAYLOAD_SECRET=... docker compose up --build
|
|
3
|
+
services:
|
|
4
|
+
db:
|
|
5
|
+
image: postgres:16-alpine
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
environment:
|
|
8
|
+
POSTGRES_USER: vitrine
|
|
9
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vitrine}
|
|
10
|
+
POSTGRES_DB: vitrine
|
|
11
|
+
volumes:
|
|
12
|
+
- pgdata:/var/lib/postgresql/data
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ['CMD-SHELL', 'pg_isready -U vitrine']
|
|
15
|
+
interval: 5s
|
|
16
|
+
timeout: 5s
|
|
17
|
+
retries: 10
|
|
18
|
+
|
|
19
|
+
app:
|
|
20
|
+
build:
|
|
21
|
+
context: .
|
|
22
|
+
restart: unless-stopped
|
|
23
|
+
environment:
|
|
24
|
+
NODE_ENV: production
|
|
25
|
+
DATABASE_URL: postgres://vitrine:${POSTGRES_PASSWORD:-vitrine}@db:5432/vitrine
|
|
26
|
+
PAYLOAD_SECRET: ${PAYLOAD_SECRET:?set PAYLOAD_SECRET}
|
|
27
|
+
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
|
28
|
+
ports:
|
|
29
|
+
- '3000:3000'
|
|
30
|
+
depends_on:
|
|
31
|
+
db:
|
|
32
|
+
condition: service_healthy
|
|
33
|
+
|
|
34
|
+
volumes:
|
|
35
|
+
pgdata:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Pure DB adapter selection logic (§18.1 of the spec). No dependencies or side
|
|
2
|
+
// effects — so it's covered by unit tests as the single source of truth for the
|
|
3
|
+
// decision table. The wrapper with real Payload adapters lives in db.ts.
|
|
4
|
+
|
|
5
|
+
export interface DbDecisionInput {
|
|
6
|
+
/** Value of DATABASE_URL (null/'' = not set). */
|
|
7
|
+
url: string | null;
|
|
8
|
+
/** Whether we managed to connect to Postgres (ping). */
|
|
9
|
+
canConnect: boolean;
|
|
10
|
+
isProd: boolean;
|
|
11
|
+
/** VITRINE_DB_STRICT=1 — disallows fallback even in dev. */
|
|
12
|
+
strict: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type DbDecision =
|
|
16
|
+
| { kind: 'postgres' }
|
|
17
|
+
| { kind: 'sqlite'; warn?: string }
|
|
18
|
+
| { kind: 'error'; message: string };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Table §18.1:
|
|
22
|
+
* url + connect → postgres
|
|
23
|
+
* url, !connect, prod|strict → error (never fail silently in production)
|
|
24
|
+
* url, !connect, dev → sqlite + warn
|
|
25
|
+
* !url, prod → error
|
|
26
|
+
* !url, dev → sqlite
|
|
27
|
+
*/
|
|
28
|
+
export function decideDbAdapter(input: DbDecisionInput): DbDecision {
|
|
29
|
+
const { url, canConnect, isProd, strict } = input;
|
|
30
|
+
|
|
31
|
+
if (url) {
|
|
32
|
+
if (canConnect) return { kind: 'postgres' };
|
|
33
|
+
if (isProd || strict) {
|
|
34
|
+
return { kind: 'error', message: '[vitrine] DATABASE_URL is set, but the DB is unreachable' };
|
|
35
|
+
}
|
|
36
|
+
return { kind: 'sqlite', warn: '[vitrine] Postgres unreachable → falling back to SQLite (dev)' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isProd) {
|
|
40
|
+
return { kind: 'error', message: '[vitrine] DATABASE_URL is required in production' };
|
|
41
|
+
}
|
|
42
|
+
return { kind: 'sqlite' };
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Short TCP ping to Postgres with a timeout (~2s), then close. Uses node:net,
|
|
2
|
+
// no driver dependency — no need to distinguish unavailable/timeout/error:
|
|
3
|
+
// any failure in dev → fallback (see db-decision.ts).
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
|
|
6
|
+
export function canConnectPostgres(url: string, timeoutMs = 2000): Promise<boolean> {
|
|
7
|
+
let host = 'localhost';
|
|
8
|
+
let port = 5432;
|
|
9
|
+
try {
|
|
10
|
+
const u = new URL(url);
|
|
11
|
+
if (u.hostname) host = u.hostname;
|
|
12
|
+
if (u.port) port = Number(u.port);
|
|
13
|
+
} catch {
|
|
14
|
+
return Promise.resolve(false);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const socket = new net.Socket();
|
|
19
|
+
const finish = (ok: boolean): void => {
|
|
20
|
+
socket.destroy();
|
|
21
|
+
resolve(ok);
|
|
22
|
+
};
|
|
23
|
+
socket.setTimeout(timeoutMs);
|
|
24
|
+
socket.once('connect', () => finish(true));
|
|
25
|
+
socket.once('timeout', () => finish(false));
|
|
26
|
+
socket.once('error', () => finish(false));
|
|
27
|
+
socket.connect(port, host);
|
|
28
|
+
});
|
|
29
|
+
}
|