@vitrine-kit/vitrine 0.4.3 → 0.5.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 +1 -1
- package/dist/index.js +114 -45
- package/kit/registry/_index.json +70 -9
- package/kit/registry/accounts/docs/accounts.md +20 -0
- package/kit/registry/accounts/feature.json +55 -0
- package/kit/registry/accounts/files/app/(frontend)/account/forgot-password/page.tsx +24 -0
- package/kit/registry/accounts/files/app/(frontend)/account/login/page.tsx +29 -0
- package/kit/registry/accounts/files/app/(frontend)/account/orders/page.tsx +90 -0
- package/kit/registry/accounts/files/app/(frontend)/account/page.tsx +86 -0
- package/kit/registry/accounts/files/app/(frontend)/account/register/page.tsx +24 -0
- package/kit/registry/accounts/files/app/(frontend)/account/reset-password/page.tsx +25 -0
- package/kit/registry/accounts/files/app/api/account/orders/route.ts +39 -0
- package/kit/registry/accounts/files/components/accounts/AccountLink.tsx +15 -0
- package/kit/registry/accounts/files/components/accounts/ForgotPasswordForm.tsx +90 -0
- package/kit/registry/accounts/files/components/accounts/LoginForm.tsx +84 -0
- package/kit/registry/accounts/files/components/accounts/LogoutButton.tsx +31 -0
- package/kit/registry/accounts/files/components/accounts/RegisterForm.tsx +108 -0
- package/kit/registry/accounts/files/components/accounts/ResetPasswordForm.tsx +84 -0
- package/kit/registry/accounts/files/lib/accounts/blueprint.ts +47 -0
- package/kit/registry/accounts/files/lib/accounts/register.ts +6 -0
- package/kit/registry/accounts/files/lib/accounts/session.ts +18 -0
- package/kit/registry/cart/feature.json +1 -1
- package/kit/registry/cart/files/app/api/cart/route.ts +46 -8
- package/kit/registry/cart/files/components/cart/AddToCart.tsx +38 -3
- package/kit/registry/cart/files/components/cart/CartIndicator.tsx +27 -2
- package/kit/registry/cart/files/components/cart/CartLineItem.tsx +71 -13
- package/kit/registry/cart/files/components/cart/CartView.tsx +9 -7
- package/kit/registry/catalog/feature.json +1 -1
- package/kit/registry/catalog/files/lib/catalog/data.ts +73 -5
- package/kit/registry/checkout/docs/checkout.md +19 -1
- package/kit/registry/checkout/feature.json +1 -1
- package/kit/registry/checkout/files/app/(frontend)/order/success/page.tsx +49 -0
- package/kit/registry/checkout/files/app/api/checkout/route.ts +14 -3
- package/kit/registry/checkout/files/components/checkout/CheckoutButton.tsx +5 -3
- package/kit/registry/checkout/files/lib/checkout/fulfill.ts +22 -0
- package/kit/registry/checkout/files/lib/checkout/notify.ts +52 -0
- package/kit/registry/checkout-paddle/feature.json +1 -1
- package/kit/registry/checkout-stripe/feature.json +1 -1
- package/kit/registry/checkout-yookassa/feature.json +1 -1
- package/kit/registry/email/docs/email.md +19 -0
- package/kit/registry/email/feature.json +60 -0
- package/kit/registry/email/files/lib/email/adapter.ts +89 -0
- package/kit/registry/filters/docs/filters.md +13 -0
- package/kit/registry/filters/feature.json +45 -0
- package/kit/registry/filters/files/components/filters/CatalogToolbar.tsx +123 -0
- package/kit/registry/filters/files/lib/filters/register.ts +6 -0
- package/kit/registry/i18n/docs/i18n.md +24 -0
- package/kit/registry/i18n/feature.json +47 -0
- package/kit/registry/i18n/files/components/i18n/LocaleSwitcher.tsx +64 -0
- package/kit/registry/i18n/files/lib/i18n/dictionary.ts +52 -0
- package/kit/registry/i18n/files/lib/i18n/locale.ts +11 -0
- package/kit/registry/i18n/files/lib/i18n/register.ts +6 -0
- package/kit/registry/i18n/files/lib/i18n/t.ts +1 -0
- package/kit/registry/i18n/files/lib/i18n/useChromeLabel.ts +28 -0
- package/kit/registry/i18n/files/middleware.ts +51 -0
- package/kit/registry/product-page/feature.json +1 -1
- package/kit/registry/product-page/files/components/product/ProductGallery.tsx +23 -10
- package/kit/registry/product-page/files/components/product/ProductView.tsx +3 -3
- package/kit/registry/reviews/docs/reviews.md +11 -0
- package/kit/registry/reviews/feature.json +55 -0
- package/kit/registry/reviews/files/app/api/reviews/route.ts +55 -0
- package/kit/registry/reviews/files/components/reviews/ReviewList.tsx +119 -0
- package/kit/registry/reviews/files/lib/reviews/blueprint.ts +13 -0
- package/kit/registry/reviews/files/lib/reviews/register.ts +6 -0
- package/kit/registry/reviews/files/lib/reviews/types.ts +22 -0
- package/kit/registry/search/docs/search.md +12 -0
- package/kit/registry/search/feature.json +49 -0
- package/kit/registry/search/files/app/(frontend)/search/page.tsx +34 -0
- package/kit/registry/search/files/components/search/SearchForm.tsx +32 -0
- package/kit/registry/search/files/components/search/SearchResults.tsx +87 -0
- package/kit/registry/search/files/lib/search/data.ts +8 -0
- package/kit/registry/search/files/lib/search/register.ts +8 -0
- package/kit/registry/seo/docs/seo.md +4 -1
- package/kit/registry/seo/feature.json +5 -1
- package/kit/registry/seo/files/app/robots.ts +13 -0
- package/kit/registry/seo/files/app/sitemap.ts +27 -0
- package/kit/registry/stubs/route-glue.d.ts +71 -0
- package/kit/registry/tsconfig.json +7 -2
- package/kit/registry/wishlist/docs/wishlist.md +11 -0
- package/kit/registry/wishlist/feature.json +55 -0
- package/kit/registry/wishlist/files/app/(frontend)/wishlist/page.tsx +16 -0
- package/kit/registry/wishlist/files/components/wishlist/WishlistButton.tsx +37 -0
- package/kit/registry/wishlist/files/components/wishlist/WishlistIndicator.tsx +30 -0
- package/kit/registry/wishlist/files/components/wishlist/WishlistView.tsx +73 -0
- package/kit/registry/wishlist/files/lib/wishlist/register.ts +8 -0
- package/kit/registry/wishlist/files/lib/wishlist/storage.ts +30 -0
- package/kit/templates/backend-payload/files/Dockerfile +3 -0
- package/kit/templates/backend-payload/files/docker-compose.yml +8 -0
- package/kit/templates/backend-payload/files/lib/adapter/payload-catalog.ts +51 -17
- package/kit/templates/backend-payload/files/lib/adapter/payload-commerce.ts +18 -0
- package/kit/templates/backend-payload/files/lib/adapter/product-query.ts +126 -0
- package/kit/templates/backend-payload/files/lib/seed/admin.ts +7 -3
- package/kit/templates/backend-payload/files/lib/seed/guards.ts +17 -4
- package/kit/templates/backend-payload/files/lib/seed/locales.ts +53 -0
- package/kit/templates/backend-payload/files/lib/seed/run.ts +57 -6
- package/kit/templates/backend-payload/files/next.config.mjs +8 -2
- package/kit/templates/backend-payload/files/payload.config.ts +27 -3
- package/kit/templates/backend-vendure/files/Dockerfile +1 -1
- package/kit/templates/backend-vendure/files/docker-compose.yml +2 -1
- package/kit/templates/backend-vendure/files/lib/adapter/product-query.ts +126 -0
- package/kit/templates/backend-vendure/files/lib/adapter/vendure-catalog.ts +18 -11
- package/kit/templates/base/files/.claude/commands/update.md +1 -1
- package/kit/templates/base/files/app/(frontend)/categories/[slug]/page.tsx +51 -4
- package/kit/templates/base/files/app/(frontend)/layout.tsx +13 -7
- package/kit/templates/base/files/app/(frontend)/not-found.tsx +26 -0
- package/kit/templates/base/files/app/(frontend)/page.tsx +49 -4
- package/kit/templates/base/files/app/(frontend)/products/[slug]/page.tsx +5 -2
- package/kit/templates/base/files/app/globals.css +13 -0
- package/kit/templates/base/files/components/SlotsProvider.tsx +19 -0
- package/kit/templates/base/files/components/layout/SiteFooter.tsx +6 -1
- package/kit/templates/base/files/components/layout/SiteHeader.tsx +8 -2
- package/kit/templates/base/files/lib/i18n/dictionary.ts +36 -0
- package/kit/templates/base/files/lib/i18n/locale.ts +11 -0
- package/kit/templates/base/files/lib/i18n/useChromeLabel.ts +28 -0
- package/kit/templates/base/files/lib/site.ts +3 -0
- package/kit/templates/base/files/next.config.mjs +8 -2
- package/package.json +2 -2
|
@@ -1,21 +1,59 @@
|
|
|
1
1
|
// Category listing. Filtered by category slug via the CatalogSource contract.
|
|
2
|
+
import type { Metadata } from 'next';
|
|
2
3
|
import { notFound } from 'next/navigation';
|
|
3
4
|
import { Slot } from '@vitrine-kit/core/react';
|
|
4
5
|
import { getCatalogSource } from '@/lib/adapter';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
collectOptionFacets,
|
|
8
|
+
loadCategories,
|
|
9
|
+
loadProducts,
|
|
10
|
+
parsePriceBound,
|
|
11
|
+
parseProductFilters,
|
|
12
|
+
parseProductSort,
|
|
13
|
+
} from '@/lib/catalog/data';
|
|
14
|
+
import { getRequestLocale } from '@/lib/i18n/locale';
|
|
6
15
|
import { ProductGrid } from '@/components/catalog/ProductGrid';
|
|
16
|
+
import { siteName } from '@/lib/site';
|
|
7
17
|
|
|
8
18
|
interface PageProps {
|
|
9
19
|
params: Promise<{ slug: string }>;
|
|
20
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
10
21
|
}
|
|
11
22
|
|
|
12
|
-
export
|
|
23
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
13
24
|
const { slug } = await params;
|
|
25
|
+
const locale = await getRequestLocale();
|
|
14
26
|
const source = await getCatalogSource();
|
|
15
|
-
const category = (await loadCategories(source)).find((c) => c.slug === slug);
|
|
27
|
+
const category = (await loadCategories(source, locale)).find((c) => c.slug === slug);
|
|
28
|
+
if (!category) return {};
|
|
29
|
+
return {
|
|
30
|
+
title: category.title,
|
|
31
|
+
description: category.description ?? `${category.title} — ${siteName}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default async function CategoryPage({ params, searchParams }: PageProps) {
|
|
36
|
+
const { slug } = await params;
|
|
37
|
+
const sp = await searchParams;
|
|
38
|
+
const sort = parseProductSort(typeof sp.sort === 'string' ? sp.sort : undefined) ?? 'newest';
|
|
39
|
+
const filters = parseProductFilters(sp);
|
|
40
|
+
const priceMin = parsePriceBound(sp.priceMin);
|
|
41
|
+
const priceMax = parsePriceBound(sp.priceMax);
|
|
42
|
+
const locale = await getRequestLocale();
|
|
43
|
+
const source = await getCatalogSource();
|
|
44
|
+
const category = (await loadCategories(source, locale)).find((c) => c.slug === slug);
|
|
16
45
|
if (!category) notFound();
|
|
17
46
|
|
|
18
|
-
const
|
|
47
|
+
const facetSource = await loadProducts(source, { category: slug, perPage: 100, locale });
|
|
48
|
+
const facets = collectOptionFacets(facetSource);
|
|
49
|
+
const products = await loadProducts(source, {
|
|
50
|
+
category: slug,
|
|
51
|
+
sort,
|
|
52
|
+
filters,
|
|
53
|
+
priceMin,
|
|
54
|
+
priceMax,
|
|
55
|
+
locale,
|
|
56
|
+
});
|
|
19
57
|
|
|
20
58
|
return (
|
|
21
59
|
<div className="flex flex-col gap-section">
|
|
@@ -26,6 +64,15 @@ export default async function CategoryPage({ params }: PageProps) {
|
|
|
26
64
|
<p className="max-w-prose text-muted-fg">{category.description}</p>
|
|
27
65
|
) : null}
|
|
28
66
|
</header>
|
|
67
|
+
<Slot
|
|
68
|
+
name="catalog.toolbar"
|
|
69
|
+
sort={sort}
|
|
70
|
+
action={`/categories/${slug}`}
|
|
71
|
+
facets={facets}
|
|
72
|
+
filters={filters ?? {}}
|
|
73
|
+
priceMin={typeof sp.priceMin === 'string' ? sp.priceMin : ''}
|
|
74
|
+
priceMax={typeof sp.priceMax === 'string' ? sp.priceMax : ''}
|
|
75
|
+
/>
|
|
29
76
|
<ProductGrid products={products} />
|
|
30
77
|
<Slot name="category.below-products" />
|
|
31
78
|
</div>
|
|
@@ -5,27 +5,33 @@ import type { ReactNode } from 'react';
|
|
|
5
5
|
import type { Metadata } from 'next';
|
|
6
6
|
import '../globals.css';
|
|
7
7
|
import { siteConfig } from '@/site.config';
|
|
8
|
+
import { baseUrl, siteName } from '@/lib/site';
|
|
8
9
|
import { registerSlots } from '@/lib/slots';
|
|
9
10
|
import { Slot } from '@vitrine-kit/core/react';
|
|
11
|
+
import { SlotsProvider } from '@/components/SlotsProvider';
|
|
10
12
|
import { SiteHeader } from '@/components/layout/SiteHeader';
|
|
11
13
|
import { SiteFooter } from '@/components/layout/SiteFooter';
|
|
12
14
|
|
|
13
|
-
// Register installed features' slots (lib/slots.ts is generated by the CLI).
|
|
15
|
+
// Register installed features' slots for SSR (lib/slots.ts is generated by the CLI).
|
|
16
|
+
// SlotsProvider re-runs the same registration in the browser so <Slot> hydrates.
|
|
14
17
|
registerSlots();
|
|
15
18
|
|
|
16
19
|
export const metadata: Metadata = {
|
|
17
|
-
|
|
20
|
+
metadataBase: new URL(baseUrl),
|
|
21
|
+
title: { default: siteName, template: `%s · ${siteName}` },
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
export default function FrontendLayout({ children }: { children: ReactNode }) {
|
|
21
25
|
return (
|
|
22
26
|
<html lang={siteConfig.i18n.defaultLocale}>
|
|
23
27
|
<body className="min-h-screen bg-bg font-sans text-fg antialiased">
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
<SlotsProvider>
|
|
29
|
+
<Slot name="global.banner-top" />
|
|
30
|
+
<SiteHeader />
|
|
31
|
+
<main className="mx-auto w-full max-w-container px-gutter py-section">{children}</main>
|
|
32
|
+
<SiteFooter />
|
|
33
|
+
<Slot name="global.body-end" />
|
|
34
|
+
</SlotsProvider>
|
|
29
35
|
</body>
|
|
30
36
|
</html>
|
|
31
37
|
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { siteName } from '@/lib/site';
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: 'Page not found',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function NotFound() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="vt-not-found flex flex-col gap-section">
|
|
11
|
+
<header className="flex flex-col gap-unit">
|
|
12
|
+
<p className="text-sm text-muted-fg">404</p>
|
|
13
|
+
<h1 className="font-heading text-3xl text-fg">Page not found</h1>
|
|
14
|
+
<p className="max-w-prose text-muted-fg">
|
|
15
|
+
That page is missing or the product is no longer available in {siteName}.
|
|
16
|
+
</p>
|
|
17
|
+
</header>
|
|
18
|
+
<a
|
|
19
|
+
href="/"
|
|
20
|
+
className="w-fit rounded-md bg-primary px-gutter py-unit text-primary-fg transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 ring-ring"
|
|
21
|
+
>
|
|
22
|
+
Back to catalog
|
|
23
|
+
</a>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// Storefront home: catalog listing. Data via the CatalogSource contract
|
|
2
2
|
// (backend-agnostic); the grid is the catalog feature's component.
|
|
3
|
+
import type { Metadata } from 'next';
|
|
3
4
|
import { Slot } from '@vitrine-kit/core/react';
|
|
4
5
|
import { getCatalogSource } from '@/lib/adapter';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
collectOptionFacets,
|
|
8
|
+
loadProducts,
|
|
9
|
+
parsePriceBound,
|
|
10
|
+
parseProductFilters,
|
|
11
|
+
parseProductSort,
|
|
12
|
+
} from '@/lib/catalog/data';
|
|
13
|
+
import { getRequestLocale } from '@/lib/i18n/locale';
|
|
6
14
|
import { ProductGrid } from '@/components/catalog/ProductGrid';
|
|
15
|
+
import { siteName } from '@/lib/site';
|
|
7
16
|
|
|
8
17
|
function DemoHero() {
|
|
9
18
|
return (
|
|
@@ -13,7 +22,7 @@ function DemoHero() {
|
|
|
13
22
|
>
|
|
14
23
|
<p className="text-sm uppercase tracking-wide text-muted-fg">Demo catalog</p>
|
|
15
24
|
<h1 id="demo-hero-heading" className="font-heading text-3xl text-fg md:text-4xl">
|
|
16
|
-
|
|
25
|
+
{siteName}
|
|
17
26
|
</h1>
|
|
18
27
|
<p className="max-w-prose text-muted-fg">
|
|
19
28
|
Zero-config seed data is loaded for local development — five products across Apparel and
|
|
@@ -38,9 +47,36 @@ function DemoHero() {
|
|
|
38
47
|
);
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
interface PageProps {
|
|
51
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
55
|
+
return {
|
|
56
|
+
title: { absolute: siteName },
|
|
57
|
+
description: `${siteName} — product catalog.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default async function HomePage({ searchParams }: PageProps) {
|
|
62
|
+
const params = await searchParams;
|
|
63
|
+
const sort = parseProductSort(typeof params.sort === 'string' ? params.sort : undefined) ?? 'newest';
|
|
64
|
+
const filters = parseProductFilters(params);
|
|
65
|
+
const priceMin = parsePriceBound(params.priceMin);
|
|
66
|
+
const priceMax = parsePriceBound(params.priceMax);
|
|
67
|
+
const locale = await getRequestLocale();
|
|
42
68
|
const source = await getCatalogSource();
|
|
43
|
-
|
|
69
|
+
|
|
70
|
+
const facetSource = await loadProducts(source, { perPage: 100, locale });
|
|
71
|
+
const facets = collectOptionFacets(facetSource);
|
|
72
|
+
const products = await loadProducts(source, {
|
|
73
|
+
perPage: 12,
|
|
74
|
+
sort,
|
|
75
|
+
filters,
|
|
76
|
+
priceMin,
|
|
77
|
+
priceMax,
|
|
78
|
+
locale,
|
|
79
|
+
});
|
|
44
80
|
|
|
45
81
|
return (
|
|
46
82
|
<div className="flex flex-col gap-section">
|
|
@@ -49,6 +85,15 @@ export default async function HomePage() {
|
|
|
49
85
|
<h2 id="catalog-heading" className="font-heading text-fg">
|
|
50
86
|
Catalog
|
|
51
87
|
</h2>
|
|
88
|
+
<Slot
|
|
89
|
+
name="catalog.toolbar"
|
|
90
|
+
sort={sort}
|
|
91
|
+
action="/"
|
|
92
|
+
facets={facets}
|
|
93
|
+
filters={filters ?? {}}
|
|
94
|
+
priceMin={typeof params.priceMin === 'string' ? params.priceMin : ''}
|
|
95
|
+
priceMax={typeof params.priceMax === 'string' ? params.priceMax : ''}
|
|
96
|
+
/>
|
|
52
97
|
<Slot name="catalog.grid-top" />
|
|
53
98
|
<ProductGrid products={products} />
|
|
54
99
|
<Slot name="catalog.grid-bottom" />
|
|
@@ -4,6 +4,7 @@ import type { Metadata } from 'next';
|
|
|
4
4
|
import { notFound } from 'next/navigation';
|
|
5
5
|
import { getCatalogSource } from '@/lib/adapter';
|
|
6
6
|
import { loadProduct } from '@/lib/catalog/data';
|
|
7
|
+
import { getRequestLocale } from '@/lib/i18n/locale';
|
|
7
8
|
import { ProductView } from '@/components/product/ProductView';
|
|
8
9
|
import { buildProductMetadata } from '@/lib/seo/metadata';
|
|
9
10
|
import { productJsonLd } from '@/lib/seo/jsonld';
|
|
@@ -16,8 +17,9 @@ interface PageProps {
|
|
|
16
17
|
|
|
17
18
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
18
19
|
const { slug } = await params;
|
|
20
|
+
const locale = await getRequestLocale();
|
|
19
21
|
const source = await getCatalogSource();
|
|
20
|
-
const product = await loadProduct(source, slug);
|
|
22
|
+
const product = await loadProduct(source, slug, locale);
|
|
21
23
|
if (!product) return {};
|
|
22
24
|
const meta = buildProductMetadata(product, { baseUrl });
|
|
23
25
|
return {
|
|
@@ -30,8 +32,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|
|
30
32
|
|
|
31
33
|
export default async function ProductPage({ params }: PageProps) {
|
|
32
34
|
const { slug } = await params;
|
|
35
|
+
const locale = await getRequestLocale();
|
|
33
36
|
const source = await getCatalogSource();
|
|
34
|
-
const product = await loadProduct(source, slug);
|
|
37
|
+
const product = await loadProduct(source, slug, locale);
|
|
35
38
|
if (!product) notFound();
|
|
36
39
|
|
|
37
40
|
return (
|
|
@@ -4,3 +4,16 @@
|
|
|
4
4
|
@tailwind base;
|
|
5
5
|
@tailwind components;
|
|
6
6
|
@tailwind utilities;
|
|
7
|
+
|
|
8
|
+
/* Fallback if a design theme strips utilities — keep search label accessible. */
|
|
9
|
+
.sr-only {
|
|
10
|
+
position: absolute;
|
|
11
|
+
width: 1px;
|
|
12
|
+
height: 1px;
|
|
13
|
+
padding: 0;
|
|
14
|
+
margin: -1px;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
clip: rect(0, 0, 0, 0);
|
|
17
|
+
white-space: nowrap;
|
|
18
|
+
border-width: 0;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ensures feature slot mounts are registered in the browser bundle.
|
|
2
|
+
// Server layout also calls registerSlots() for SSR; this covers hydration.
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
import { registerSlots } from '@/lib/slots';
|
|
7
|
+
|
|
8
|
+
let registered = false;
|
|
9
|
+
|
|
10
|
+
function ensureRegistered(): void {
|
|
11
|
+
if (registered) return;
|
|
12
|
+
registerSlots();
|
|
13
|
+
registered = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SlotsProvider({ children }: { children: ReactNode }) {
|
|
17
|
+
ensureRegistered();
|
|
18
|
+
return children;
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Storefront footer. Hosts the global.footer slot.
|
|
2
2
|
import { Slot } from '@vitrine-kit/core/react';
|
|
3
|
+
import { siteName } from '@/lib/site';
|
|
3
4
|
|
|
4
5
|
export function SiteFooter() {
|
|
5
6
|
return (
|
|
@@ -7,7 +8,11 @@ export function SiteFooter() {
|
|
|
7
8
|
<div className="mx-auto max-w-container px-gutter py-section text-muted-fg">
|
|
8
9
|
<Slot
|
|
9
10
|
name="global.footer"
|
|
10
|
-
fallback={
|
|
11
|
+
fallback={
|
|
12
|
+
<p>
|
|
13
|
+
© {new Date().getFullYear()} {siteName}
|
|
14
|
+
</p>
|
|
15
|
+
}
|
|
11
16
|
/>
|
|
12
17
|
</div>
|
|
13
18
|
</footer>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// CategoryNav mounts) and passes it categories from the active CatalogSource.
|
|
3
3
|
import { Slot } from '@vitrine-kit/core/react';
|
|
4
4
|
import { getCatalogSource } from '@/lib/adapter';
|
|
5
|
+
import { siteName } from '@/lib/site';
|
|
6
|
+
import { siteConfig } from '@/site.config';
|
|
5
7
|
|
|
6
8
|
export async function SiteHeader() {
|
|
7
9
|
const source = await getCatalogSource();
|
|
@@ -11,11 +13,15 @@ export async function SiteHeader() {
|
|
|
11
13
|
<header className="border-b border-border">
|
|
12
14
|
<div className="mx-auto flex max-w-container items-center justify-between gap-gutter px-gutter py-unit">
|
|
13
15
|
<a href="/" className="font-heading text-fg">
|
|
14
|
-
<Slot name="global.header-start" fallback={<span>
|
|
16
|
+
<Slot name="global.header-start" fallback={<span>{siteName}</span>} />
|
|
15
17
|
</a>
|
|
16
18
|
<Slot name="global.header-nav" categories={categories} />
|
|
17
19
|
<div className="flex items-center gap-unit">
|
|
18
|
-
<Slot
|
|
20
|
+
<Slot
|
|
21
|
+
name="global.header-actions"
|
|
22
|
+
locales={siteConfig.i18n.locales}
|
|
23
|
+
defaultLocale={siteConfig.i18n.defaultLocale}
|
|
24
|
+
/>
|
|
19
25
|
</div>
|
|
20
26
|
</div>
|
|
21
27
|
</header>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Minimal chrome dictionary (English). The `i18n` feature overwrites this file
|
|
2
|
+
// with multi-locale dictionaries when installed.
|
|
3
|
+
export type MessageKey =
|
|
4
|
+
| 'cart'
|
|
5
|
+
| 'search'
|
|
6
|
+
| 'checkout'
|
|
7
|
+
| 'wishlist'
|
|
8
|
+
| 'orders'
|
|
9
|
+
| 'account'
|
|
10
|
+
| 'continueShopping'
|
|
11
|
+
| 'addToCart'
|
|
12
|
+
| 'thankYou'
|
|
13
|
+
| 'signIn'
|
|
14
|
+
| 'createAccount';
|
|
15
|
+
|
|
16
|
+
const en: Record<MessageKey, string> = {
|
|
17
|
+
cart: 'Cart',
|
|
18
|
+
search: 'Search',
|
|
19
|
+
checkout: 'Checkout',
|
|
20
|
+
wishlist: 'Wishlist',
|
|
21
|
+
orders: 'Orders',
|
|
22
|
+
account: 'Account',
|
|
23
|
+
continueShopping: 'Continue shopping',
|
|
24
|
+
addToCart: 'Add to cart',
|
|
25
|
+
thankYou: 'Thank you for your order',
|
|
26
|
+
signIn: 'Sign in',
|
|
27
|
+
createAccount: 'Create account',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function t(key: MessageKey, _locale = 'en'): string {
|
|
31
|
+
return en[key] ?? key;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function supportedLocales(): string[] {
|
|
35
|
+
return ['en'];
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Request locale from the vitrine_locale cookie (set by i18n middleware / switcher).
|
|
2
|
+
import { cookies } from 'next/headers';
|
|
3
|
+
import { siteConfig } from '@/site.config';
|
|
4
|
+
|
|
5
|
+
export async function getRequestLocale(): Promise<string> {
|
|
6
|
+
const jar = await cookies();
|
|
7
|
+
const fromCookie = jar.get('vitrine_locale')?.value;
|
|
8
|
+
const locales = siteConfig.i18n.locales;
|
|
9
|
+
if (fromCookie && locales.includes(fromCookie)) return fromCookie;
|
|
10
|
+
return siteConfig.i18n.defaultLocale;
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Client helper: resolve chrome copy from the locale cookie + dictionary.
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { t, type MessageKey } from './dictionary.js';
|
|
6
|
+
|
|
7
|
+
const COOKIE = 'vitrine_locale';
|
|
8
|
+
|
|
9
|
+
function readLocale(fallback: string): string {
|
|
10
|
+
if (typeof document === 'undefined') return fallback;
|
|
11
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE}=([^;]*)`));
|
|
12
|
+
return match ? decodeURIComponent(match[1]!) : fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Reactive label for header/chrome controls (updates after mount from the locale cookie). */
|
|
16
|
+
export function useChromeLabel(key: MessageKey, fallbackLocale = 'en'): string {
|
|
17
|
+
const [label, setLabel] = useState(() => t(key, fallbackLocale));
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setLabel(t(key, readLocale(fallbackLocale)));
|
|
21
|
+
}, [key, fallbackLocale]);
|
|
22
|
+
|
|
23
|
+
return label;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function chromeLabel(key: MessageKey, locale = 'en'): string {
|
|
27
|
+
return t(key, locale);
|
|
28
|
+
}
|
|
@@ -5,6 +5,9 @@ import { siteConfig } from '@/site.config';
|
|
|
5
5
|
/** Base URL for canonical/OG. Overridden by NEXT_PUBLIC_SITE_URL. */
|
|
6
6
|
export const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000';
|
|
7
7
|
|
|
8
|
+
/** Storefront display name from `vitrine init` (falls back for older clients). */
|
|
9
|
+
export const siteName = siteConfig.name?.trim() || 'Store';
|
|
10
|
+
|
|
8
11
|
export const currency = siteConfig.i18n.currency;
|
|
9
12
|
export const defaultLocale = siteConfig.i18n.defaultLocale;
|
|
10
13
|
export const locales = siteConfig.i18n.locales;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Baseline security headers (OWASP A05: Security Misconfiguration) applied to every route.
|
|
2
2
|
// The CSP below is a conservative starting point — payment providers with client-side widgets
|
|
3
3
|
// (e.g. embedded Stripe Elements) may need their domains added to script-src/connect-src/frame-src.
|
|
4
|
+
// Next.js `pnpm dev` (webpack eval source maps / Fast Refresh) needs 'unsafe-eval'; omit it in
|
|
5
|
+
// production builds so the shipped CSP stays tighter.
|
|
6
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
7
|
+
const scriptSrc = isDev
|
|
8
|
+
? "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
|
9
|
+
: "script-src 'self' 'unsafe-inline'";
|
|
10
|
+
|
|
4
11
|
const securityHeaders = [
|
|
5
12
|
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
6
13
|
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
@@ -9,8 +16,7 @@ const securityHeaders = [
|
|
|
9
16
|
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
|
10
17
|
{
|
|
11
18
|
key: 'Content-Security-Policy',
|
|
12
|
-
value:
|
|
13
|
-
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none';",
|
|
19
|
+
value: `default-src 'self'; ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none';`,
|
|
14
20
|
},
|
|
15
21
|
];
|
|
16
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitrine-kit/vitrine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Vitrine CLI — feature install primitive, init (wizard), add, update, doctor, design apply.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^0.8.2",
|
|
27
27
|
"commander": "^12.1.0",
|
|
28
|
-
"@vitrine-kit/contracts": "1.
|
|
28
|
+
"@vitrine-kit/contracts": "1.3.0"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "node scripts/generate-kit-versions.mjs && tsup src/index.ts --format esm --clean && node scripts/copy-kit.mjs",
|