create-nymbal-app 0.4.0 → 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/dist/index.d.ts +2 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/scaffold/project/astro/src/pages/index.astro +13 -15
- package/scaffold/templates/astro/_dot.node-version +1 -1
- package/scaffold/templates/astro/astro.config.mjs +23 -0
- package/scaffold/templates/astro/package.json +26 -26
- package/scaffold/templates/astro/src/components/SkipNav.astro +3 -1
- package/scaffold/templates/astro/src/elements/index.ts +11 -0
- package/scaffold/templates/astro/src/layouts/BaseLayout.astro +15 -4
- package/scaffold/templates/astro/src/lib/api.ts +113 -21
- package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
- package/scaffold/templates/astro/src/lib/client.ts +4 -4
- package/scaffold/templates/astro/src/lib/fragments.ts +48 -0
- package/scaffold/templates/astro/src/lib/server-elements.ts +5 -0
- package/scaffold/templates/astro/src/middleware.ts +37 -0
- package/scaffold/templates/astro/src/pages/account/addresses.astro +5 -97
- package/scaffold/templates/astro/src/pages/account/login.astro +7 -34
- package/scaffold/templates/astro/src/pages/account/profile.astro +6 -56
- package/scaffold/templates/astro/src/pages/account/register.astro +7 -73
- package/scaffold/templates/astro/src/pages/cart.astro +14 -7
- package/scaffold/templates/astro/src/pages/category/[slug].astro +17 -21
- package/scaffold/templates/astro/src/pages/checkout.astro +30 -94
- package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -2
- package/scaffold/templates/astro/src/pages/fragments/header.astro +12 -2
- package/scaffold/templates/astro/src/pages/fragments/nav.astro +8 -5
- package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +9 -10
- package/scaffold/templates/astro/src/pages/index.astro +18 -15
- package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +22 -44
- package/scaffold/templates/astro/src/pages/product/[slug].astro +44 -30
- package/scaffold/templates/astro/src/pages/products/index.astro +16 -21
- package/scaffold/templates/astro/src/pages/search.astro +10 -11
- package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
- package/scaffold/templates/astro/src/scripts/pdp.ts +24 -21
- package/scaffold/templates/astro/src/scripts/plp.ts +15 -93
- package/scaffold/templates/astro/src/scripts/search.ts +7 -132
- package/scaffold/templates/astro/src/scripts/signed-in-redirect.ts +4 -0
- package/scaffold/templates/astro/src/styles/global.css +49 -45
- package/scaffold/templates/astro/src/styles/pages/account.css +0 -90
- package/scaffold/templates/astro/src/styles/pages/confirmation.css +6 -0
- package/scaffold/templates/astro/src/styles/pages/home.css +9 -100
- package/scaffold/templates/astro/src/styles/pages/pdp.css +5 -171
- package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
- package/scaffold/templates/astro/src/styles/pages/search.css +4 -110
- package/scaffold/templates/astro/src/styles/tokens.css +56 -0
- package/scaffold/templates/nextjs/_dot.node-version +1 -1
- package/scaffold/templates/nextjs/app/(catalog)/category/[slug]/page.tsx +74 -0
- package/scaffold/templates/nextjs/app/(catalog)/home/[key]/page.tsx +80 -0
- package/scaffold/templates/nextjs/app/(catalog)/layout.tsx +13 -0
- package/scaffold/templates/nextjs/app/(catalog)/not-found.tsx +7 -0
- package/scaffold/templates/nextjs/app/(catalog)/product/[slug]/page.tsx +182 -0
- package/scaffold/templates/nextjs/app/(catalog)/products/page.tsx +62 -0
- package/scaffold/templates/nextjs/app/(catalog)/search/SearchView.tsx +90 -0
- package/scaffold/templates/nextjs/app/(catalog)/search/page.tsx +46 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/addresses/AddressesView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/addresses/page.tsx +17 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/login/LoginView.tsx +28 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/login/page.tsx +23 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/profile/ProfileView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/profile/page.tsx +20 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/register/RegisterView.tsx +26 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/register/page.tsx +11 -0
- package/scaffold/templates/nextjs/app/(shopper)/cart/CartView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/cart/page.tsx +22 -0
- package/scaffold/templates/nextjs/app/(shopper)/checkout/CheckoutView.tsx +27 -0
- package/scaffold/templates/nextjs/app/(shopper)/checkout/page.tsx +39 -0
- package/scaffold/templates/nextjs/app/(shopper)/layout.tsx +12 -0
- package/scaffold/templates/nextjs/app/{order → (shopper)/order}/[orderNumber]/page.tsx +24 -5
- package/scaffold/templates/nextjs/app/error.tsx +3 -7
- package/scaffold/templates/nextjs/app/fragments/footer/route.ts +13 -0
- package/scaffold/templates/nextjs/app/fragments/header/[key]/route.ts +18 -0
- package/scaffold/templates/nextjs/app/fragments/nav/[key]/route.ts +23 -0
- package/scaffold/templates/nextjs/app/fragments/product/[slug]/route.ts +22 -0
- package/scaffold/templates/nextjs/app/globals.css +5 -23
- package/scaffold/templates/nextjs/app/layout.tsx +8 -5
- package/scaffold/templates/nextjs/app/not-found.tsx +9 -33
- package/scaffold/templates/nextjs/app/tokens.css +56 -0
- package/scaffold/templates/nextjs/components/client/ListingControls.tsx +87 -0
- package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +48 -6
- package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +30 -15
- package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
- package/scaffold/templates/nextjs/components/server/Header.tsx +33 -102
- package/scaffold/templates/nextjs/components/server/NotFoundView.tsx +39 -0
- package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -3
- package/scaffold/templates/nextjs/elements/index.ts +12 -0
- package/scaffold/templates/nextjs/lib/api.ts +151 -55
- package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
- package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
- package/scaffold/templates/nextjs/lib/header.ts +39 -0
- package/scaffold/templates/nextjs/lib/render.ts +14 -0
- package/scaffold/templates/nextjs/next.config.mjs +32 -0
- package/scaffold/templates/nextjs/package.json +28 -27
- package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
- package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
- package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
- package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -183
- package/scaffold/templates/nextjs/styles/pages/plp.module.css +9 -143
- package/scaffold/templates/nextjs/styles/pages/search.module.css +1 -145
- package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
- package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
- package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
- package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
- package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
- package/scaffold/templates/astro/src/components/Footer.astro +0 -108
- package/scaffold/templates/astro/src/components/Header.astro +0 -198
- package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
- package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
- package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
- package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
- package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
- package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
- package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
- package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
- package/scaffold/templates/astro/src/components/StockBadge.astro +0 -15
- package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
- package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -118
- package/scaffold/templates/astro/src/scripts/checkout.ts +0 -271
- package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
- package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
- package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
- package/scaffold/templates/astro/src/styles/pages/cart.css +0 -215
- package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
- package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
- package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -61
- package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -17
- package/scaffold/templates/nextjs/app/page.tsx +0 -63
- package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
- package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
- package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
- package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
- package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -121
- package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -482
- package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
- package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
- package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
- package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -137
- package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
- package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
- package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
- package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
- package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -181
- package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
- package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
- package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
- package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
- package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
- package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
- package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
- package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
- package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
- package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
- package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
- package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
- package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/page.tsx +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { renderElement } from '@nymbalhq/web-components/ssr'
|
|
2
|
+
import './server-elements'
|
|
3
|
+
|
|
4
|
+
// The store's header and footer, rendered by nymbal-site-header and nymbal-site-footer: edit the links here.
|
|
5
|
+
|
|
6
|
+
export const HEADER = {
|
|
7
|
+
logo: 'Nymbal',
|
|
8
|
+
'account-href': '/account',
|
|
9
|
+
links: JSON.stringify([
|
|
10
|
+
{ label: 'Apparel', href: '/category/apparel' },
|
|
11
|
+
{ label: 'Home', href: '/category/home' },
|
|
12
|
+
{ label: 'Electronics', href: '/category/electronics' },
|
|
13
|
+
{ label: 'Books', href: '/category/books' },
|
|
14
|
+
{ label: 'All Products', href: '/products' },
|
|
15
|
+
]),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FOOTER = {
|
|
19
|
+
columns: JSON.stringify([
|
|
20
|
+
{
|
|
21
|
+
heading: 'Shop',
|
|
22
|
+
links: [
|
|
23
|
+
{ label: 'All Products', href: '/products' },
|
|
24
|
+
{ label: 'Apparel', href: '/category/apparel' },
|
|
25
|
+
{ label: 'Home', href: '/category/home' },
|
|
26
|
+
{ label: 'Electronics', href: '/category/electronics' },
|
|
27
|
+
{ label: 'Books', href: '/category/books' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
heading: 'Account',
|
|
32
|
+
links: [
|
|
33
|
+
{ label: 'Login', href: '/account/login' },
|
|
34
|
+
{ label: 'Register', href: '/account/register' },
|
|
35
|
+
{ label: 'Orders', href: '/account/orders' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
heading: 'About',
|
|
40
|
+
text: 'Nymbal is a modern headless commerce platform built for developers. Fast, flexible, and fully customisable.',
|
|
41
|
+
},
|
|
42
|
+
]),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The header's light-DOM children, each rendered by the server with the slot it fills: the search bar (when the store
|
|
47
|
+
* searches) and the mini-cart with the shopper's count (none in a shared, cached header). HTML, not markup: Astro
|
|
48
|
+
* would take `slot` on a component's child as a slot of NymbalElement's own.
|
|
49
|
+
*/
|
|
50
|
+
export function headerSlots(canSearch: boolean, cartCount?: number): string {
|
|
51
|
+
const search = canSearch
|
|
52
|
+
? renderElement('nymbal-search-bar', {
|
|
53
|
+
attributes: { slot: 'search', placeholder: 'Search products...', 'debounce-ms': '300' },
|
|
54
|
+
})
|
|
55
|
+
: ''
|
|
56
|
+
const attributes: Record<string, string> = { slot: 'cart' }
|
|
57
|
+
if (cartCount !== undefined) attributes['count'] = String(cartCount)
|
|
58
|
+
return search + renderElement('nymbal-mini-cart', { attributes })
|
|
59
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import '../elements/index.js'
|
|
1
2
|
import { createNymbalClient } from '@nymbalhq/sdk'
|
|
2
|
-
import { registerClient
|
|
3
|
+
import { registerClient } from '@nymbalhq/web-components'
|
|
3
4
|
|
|
4
5
|
const client = createNymbalClient({
|
|
5
6
|
baseUrl: import.meta.env.PUBLIC_NYMBAL_API_URL ?? 'http://localhost:3001',
|
|
@@ -7,9 +8,8 @@ const client = createNymbalClient({
|
|
|
7
8
|
})
|
|
8
9
|
|
|
9
10
|
registerClient(client)
|
|
10
|
-
// A payment driver of the project's own goes here, before
|
|
11
|
-
// @nymbalhq/sdk, named by the driver of the payments adapter's client half.
|
|
12
|
-
registerComponents()
|
|
11
|
+
// A payment driver of the project's own goes here, before any element is defined: registerPaymentDriver(name, driver)
|
|
12
|
+
// from @nymbalhq/sdk, named by the driver of the payments adapter's client half.
|
|
13
13
|
client.cart.load()
|
|
14
14
|
|
|
15
15
|
export { client }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DenormalisedProduct } from '@nymbalhq/sdk'
|
|
2
|
+
import type { Category } from '@nymbalhq/types'
|
|
3
|
+
import { loadCapabilities, loadCategories, loadProduct } from './api'
|
|
4
|
+
|
|
5
|
+
// A fragment is cached by a CDN for hours, so it carries only what the API sent: when the API fails, the fragment
|
|
6
|
+
// answers an error no cache keeps, never a fallback (no categories, no search bar) a CDN would hold for its TTL.
|
|
7
|
+
|
|
8
|
+
const NOT_STORED = { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' }
|
|
9
|
+
|
|
10
|
+
function apiFailed(fragment: string, err: unknown): Response {
|
|
11
|
+
console.error(`[fragments] ${fragment}: the API failed`, err)
|
|
12
|
+
return new Response('The store API is unavailable', { status: 502, headers: NOT_STORED })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The header fragment's data: whether the store searches; a 502 `no-store` response when the API failed. */
|
|
16
|
+
export async function headerFragment(): Promise<{ canSearch: boolean } | Response> {
|
|
17
|
+
try {
|
|
18
|
+
const capabilities = await loadCapabilities()
|
|
19
|
+
return { canSearch: capabilities.search.capabilities.includes('search') }
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return apiFailed('header', err)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The nav fragment's data: the API's categories; a 502 `no-store` response when the API failed. */
|
|
26
|
+
export async function navFragment(): Promise<{ categories: Category[] } | Response> {
|
|
27
|
+
try {
|
|
28
|
+
return { categories: await loadCategories() }
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return apiFailed('nav', err)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The product fragment's data: the API's product; a 404 `no-store` response for a product the API does not have (a CDN
|
|
36
|
+
* would apply its default 404 TTL to a 404 with no Cache-Control), a 502 `no-store` one when the API failed.
|
|
37
|
+
*/
|
|
38
|
+
export async function productFragment(
|
|
39
|
+
slug: string,
|
|
40
|
+
): Promise<{ product: DenormalisedProduct } | Response> {
|
|
41
|
+
try {
|
|
42
|
+
const product = await loadProduct(slug)
|
|
43
|
+
if (product === null) return new Response('Not found', { status: 404, headers: NOT_STORED })
|
|
44
|
+
return { product }
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return apiFailed('product', err)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { registerServerElements } from '@nymbalhq/web-components/ssr'
|
|
2
|
+
import { serverElements } from '../elements/index.js'
|
|
3
|
+
|
|
4
|
+
// The store's own elements render on the server under their tags, as the browser defines them there.
|
|
5
|
+
registerServerElements(serverElements)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
2
|
+
|
|
3
|
+
/** Whether an `Accept-Encoding` value accepts gzip: a `gzip` entry (or, without one, `*`) with a q-value above 0. */
|
|
4
|
+
function acceptsGzip(header: string): boolean {
|
|
5
|
+
let star = false
|
|
6
|
+
for (const entry of header.split(',')) {
|
|
7
|
+
const [coding = '', ...params] = entry.split(';').map((part) => part.trim().toLowerCase())
|
|
8
|
+
const q = params.find((param) => param.startsWith('q='))
|
|
9
|
+
const accepted = q === undefined || Number(q.slice(2)) > 0
|
|
10
|
+
if (coding === 'gzip' || coding === 'x-gzip') return accepted
|
|
11
|
+
if (coding === '*') star = accepted
|
|
12
|
+
}
|
|
13
|
+
return star
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The standalone Node adapter compresses nothing, so a page's HTML (its styles inlined) went over the wire whole.
|
|
17
|
+
// An HTML response is gzipped as a stream when the client accepts it; everything else passes through unchanged, and a
|
|
18
|
+
// proxy that compresses sees the encoding and passes it on. Every HTML response varies on Accept-Encoding, the
|
|
19
|
+
// uncompressed one included, so a cache keeps both forms apart.
|
|
20
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
21
|
+
const response = await next()
|
|
22
|
+
const type = response.headers.get('content-type') ?? ''
|
|
23
|
+
if (!type.startsWith('text/html') || response.headers.has('content-encoding')) return response
|
|
24
|
+
const headers = new Headers(response.headers)
|
|
25
|
+
headers.append('vary', 'Accept-Encoding')
|
|
26
|
+
let body = response.body
|
|
27
|
+
if (body !== null && acceptsGzip(context.request.headers.get('accept-encoding') ?? '')) {
|
|
28
|
+
headers.set('content-encoding', 'gzip')
|
|
29
|
+
headers.delete('content-length')
|
|
30
|
+
body = body.pipeThrough(new CompressionStream('gzip'))
|
|
31
|
+
}
|
|
32
|
+
return new Response(body, {
|
|
33
|
+
status: response.status,
|
|
34
|
+
statusText: response.statusText,
|
|
35
|
+
headers,
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -1,110 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/account.css'
|
|
3
|
-
import '
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
4
4
|
import AccountLayout from '../../layouts/AccountLayout.astro'
|
|
5
|
-
import AddressForm from '../../components/AddressForm.astro'
|
|
6
5
|
---
|
|
7
6
|
|
|
8
7
|
<AccountLayout title="Addresses">
|
|
9
8
|
<h1 class="account-page-title">Addresses</h1>
|
|
10
9
|
|
|
11
|
-
<
|
|
12
|
-
<p style="color: var(--nymbal-color-text-muted, #71717a); font-size: var(--nymbal-font-size-sm, 0.875rem);">
|
|
13
|
-
Loading addresses...
|
|
14
|
-
</p>
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
<div style="margin-top: var(--nymbal-spacing-xl, 2rem);">
|
|
18
|
-
<h2 class="section-title">Add New Address</h2>
|
|
19
|
-
<form id="add-address-form" class="account-form" data-testid="add-address-form">
|
|
20
|
-
<AddressForm id="new-address" />
|
|
21
|
-
<div class="account-form-actions">
|
|
22
|
-
<button type="submit" class="btn btn-primary" data-testid="save-address">Save Address</button>
|
|
23
|
-
</div>
|
|
24
|
-
</form>
|
|
25
|
-
<p class="form-message" id="address-form-message" style="display:none; margin-top: var(--nymbal-spacing-md, 1rem); font-size: var(--nymbal-font-size-sm, 0.875rem);"></p>
|
|
26
|
-
</div>
|
|
10
|
+
<NymbalElement tag="nymbal-address-book" />
|
|
27
11
|
</AccountLayout>
|
|
28
12
|
|
|
29
13
|
<script>
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
32
|
-
import { escapeHtml } from '../../lib/format.js'
|
|
33
|
-
import { submitOnce } from '../../lib/submit-once.js'
|
|
34
|
-
|
|
35
|
-
const container = document.getElementById('addresses-container')
|
|
36
|
-
const form = document.getElementById('add-address-form') as HTMLFormElement | null
|
|
37
|
-
const messageEl = document.getElementById('address-form-message')
|
|
38
|
-
|
|
39
|
-
function renderAddress(addr: CustomerAddress): string {
|
|
40
|
-
const isDefault = addr.isDefaultShipping || addr.isDefaultBilling
|
|
41
|
-
return `
|
|
42
|
-
<div class="address-card ${isDefault ? 'default' : ''}" data-testid="address-${escapeHtml(addr.id)}">
|
|
43
|
-
${isDefault ? '<span class="address-card-badge">Default</span>' : ''}
|
|
44
|
-
<div class="address-card-line">${escapeHtml(`${addr.firstName} ${addr.lastName}`)}</div>
|
|
45
|
-
<div class="address-card-line">${escapeHtml(addr.addressLine1)}</div>
|
|
46
|
-
${addr.addressLine2 ? `<div class="address-card-line">${escapeHtml(addr.addressLine2)}</div>` : ''}
|
|
47
|
-
<div class="address-card-line">${escapeHtml(`${addr.city}, ${addr.region} ${addr.postalCode}`)}</div>
|
|
48
|
-
<div class="address-card-line">${escapeHtml(addr.country)}</div>
|
|
49
|
-
</div>
|
|
50
|
-
`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function loadAddresses() {
|
|
54
|
-
if (!container) return
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const addresses = await client.customer.addresses.list()
|
|
58
|
-
|
|
59
|
-
if (addresses.length === 0) {
|
|
60
|
-
container.innerHTML = `
|
|
61
|
-
<div class="account-empty">
|
|
62
|
-
<h2 class="account-empty-title">No saved addresses</h2>
|
|
63
|
-
<p class="account-empty-text">Add an address below to get started.</p>
|
|
64
|
-
</div>
|
|
65
|
-
`
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
container.innerHTML = `<div class="address-list">${addresses.map(renderAddress).join('')}</div>`
|
|
70
|
-
} catch {
|
|
71
|
-
container.innerHTML = `
|
|
72
|
-
<div class="account-empty">
|
|
73
|
-
<p class="account-empty-text">Unable to load addresses.</p>
|
|
74
|
-
</div>
|
|
75
|
-
`
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
submitOnce(form, async () => {
|
|
80
|
-
if (!form || !messageEl) return
|
|
81
|
-
|
|
82
|
-
const formData = new FormData(form)
|
|
83
|
-
const field = (name: string) => String(formData.get(name) ?? '').trim()
|
|
84
|
-
const address: Address = {
|
|
85
|
-
firstName: field('firstName'),
|
|
86
|
-
lastName: field('lastName'),
|
|
87
|
-
addressLine1: field('addressLine1'),
|
|
88
|
-
city: field('city'),
|
|
89
|
-
region: field('region'),
|
|
90
|
-
postalCode: field('postalCode'),
|
|
91
|
-
country: field('country'),
|
|
92
|
-
...(field('addressLine2') && { addressLine2: field('addressLine2') }),
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
await client.customer.addresses.create(address)
|
|
97
|
-
messageEl.textContent = 'Address saved successfully.'
|
|
98
|
-
messageEl.style.color = 'var(--nymbal-color-success, #16a34a)'
|
|
99
|
-
messageEl.style.display = 'block'
|
|
100
|
-
form.reset()
|
|
101
|
-
await loadAddresses()
|
|
102
|
-
} catch {
|
|
103
|
-
messageEl.textContent = 'Failed to save address. Please try again.'
|
|
104
|
-
messageEl.style.color = 'var(--nymbal-color-error, #dc2626)'
|
|
105
|
-
messageEl.style.display = 'block'
|
|
106
|
-
}
|
|
107
|
-
})
|
|
14
|
+
import '../../lib/client.js'
|
|
15
|
+
import { NymbalAddressBook } from '@nymbalhq/web-components/elements/nymbal-address-book'
|
|
108
16
|
|
|
109
|
-
|
|
17
|
+
NymbalAddressBook.define()
|
|
110
18
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/account.css'
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
4
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -9,39 +10,7 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
|
9
10
|
<h1 class="auth-card-title">Sign In</h1>
|
|
10
11
|
<p class="auth-card-subtitle">Welcome back. Enter your credentials to continue.</p>
|
|
11
12
|
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
<form id="login-form" data-testid="login-form">
|
|
15
|
-
<div class="form-group">
|
|
16
|
-
<label for="login-email">Email</label>
|
|
17
|
-
<input
|
|
18
|
-
type="email"
|
|
19
|
-
id="login-email"
|
|
20
|
-
name="email"
|
|
21
|
-
required
|
|
22
|
-
autocomplete="email"
|
|
23
|
-
placeholder="you@example.com"
|
|
24
|
-
data-testid="login-email"
|
|
25
|
-
/>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div class="form-group">
|
|
29
|
-
<label for="login-password">Password</label>
|
|
30
|
-
<input
|
|
31
|
-
type="password"
|
|
32
|
-
id="login-password"
|
|
33
|
-
name="password"
|
|
34
|
-
required
|
|
35
|
-
autocomplete="current-password"
|
|
36
|
-
placeholder="Your password"
|
|
37
|
-
data-testid="login-password"
|
|
38
|
-
/>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<button type="submit" class="btn btn-primary" data-testid="login-submit">
|
|
42
|
-
Sign In
|
|
43
|
-
</button>
|
|
44
|
-
</form>
|
|
13
|
+
<NymbalElement tag="nymbal-login-form" />
|
|
45
14
|
|
|
46
15
|
<p class="auth-footer">
|
|
47
16
|
Don't have an account? <a href="/account/register">Create one</a>
|
|
@@ -51,5 +20,9 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
|
51
20
|
</BaseLayout>
|
|
52
21
|
|
|
53
22
|
<script>
|
|
54
|
-
import '../../
|
|
23
|
+
import '../../lib/client.js'
|
|
24
|
+
import { NymbalLoginForm } from '@nymbalhq/web-components/elements/nymbal-login-form'
|
|
25
|
+
import '../../scripts/signed-in-redirect.js'
|
|
26
|
+
|
|
27
|
+
NymbalLoginForm.define()
|
|
55
28
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/account.css'
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
4
|
import AccountLayout from '../../layouts/AccountLayout.astro'
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -7,63 +8,12 @@ import AccountLayout from '../../layouts/AccountLayout.astro'
|
|
|
7
8
|
<h1 class="account-page-title">Profile</h1>
|
|
8
9
|
<p class="account-page-subtitle">Update your personal information.</p>
|
|
9
10
|
|
|
10
|
-
<
|
|
11
|
-
<div class="account-form-row">
|
|
12
|
-
<div class="form-group">
|
|
13
|
-
<label for="profile-firstName">First Name</label>
|
|
14
|
-
<input
|
|
15
|
-
type="text"
|
|
16
|
-
id="profile-firstName"
|
|
17
|
-
name="firstName"
|
|
18
|
-
autocomplete="given-name"
|
|
19
|
-
data-testid="profile-firstName"
|
|
20
|
-
/>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="form-group">
|
|
24
|
-
<label for="profile-lastName">Last Name</label>
|
|
25
|
-
<input
|
|
26
|
-
type="text"
|
|
27
|
-
id="profile-lastName"
|
|
28
|
-
name="lastName"
|
|
29
|
-
autocomplete="family-name"
|
|
30
|
-
data-testid="profile-lastName"
|
|
31
|
-
/>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div class="form-group">
|
|
36
|
-
<label for="profile-email">Email</label>
|
|
37
|
-
<input
|
|
38
|
-
type="email"
|
|
39
|
-
id="profile-email"
|
|
40
|
-
name="email"
|
|
41
|
-
disabled
|
|
42
|
-
autocomplete="email"
|
|
43
|
-
data-testid="profile-email"
|
|
44
|
-
style="opacity: 0.6; cursor: not-allowed;"
|
|
45
|
-
/>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div class="form-group">
|
|
49
|
-
<label for="profile-phone">Phone</label>
|
|
50
|
-
<input
|
|
51
|
-
type="tel"
|
|
52
|
-
id="profile-phone"
|
|
53
|
-
name="phone"
|
|
54
|
-
autocomplete="tel"
|
|
55
|
-
data-testid="profile-phone"
|
|
56
|
-
/>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<div class="account-form-actions">
|
|
60
|
-
<button type="submit" class="btn btn-primary" data-testid="profile-save">Save Changes</button>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<p class="form-message" id="profile-message" style="display:none; margin-top: var(--nymbal-spacing-md, 1rem); font-size: var(--nymbal-font-size-sm, 0.875rem);"></p>
|
|
64
|
-
</form>
|
|
11
|
+
<NymbalElement tag="nymbal-profile-form" attributes={{ class: 'account-form' }} />
|
|
65
12
|
</AccountLayout>
|
|
66
13
|
|
|
67
14
|
<script>
|
|
68
|
-
import '../../
|
|
15
|
+
import '../../lib/client.js'
|
|
16
|
+
import { NymbalProfileForm } from '@nymbalhq/web-components/elements/nymbal-profile-form'
|
|
17
|
+
|
|
18
|
+
NymbalProfileForm.define()
|
|
69
19
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/account.css'
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
4
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -9,78 +10,7 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
|
9
10
|
<h1 class="auth-card-title">Create Account</h1>
|
|
10
11
|
<p class="auth-card-subtitle">Join us and start shopping.</p>
|
|
11
12
|
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
<form id="register-form" data-testid="register-form">
|
|
15
|
-
<div class="account-form-row">
|
|
16
|
-
<div class="form-group">
|
|
17
|
-
<label for="register-firstName">First Name</label>
|
|
18
|
-
<input
|
|
19
|
-
type="text"
|
|
20
|
-
id="register-firstName"
|
|
21
|
-
name="firstName"
|
|
22
|
-
autocomplete="given-name"
|
|
23
|
-
placeholder="First name"
|
|
24
|
-
data-testid="register-firstName"
|
|
25
|
-
/>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div class="form-group">
|
|
29
|
-
<label for="register-lastName">Last Name</label>
|
|
30
|
-
<input
|
|
31
|
-
type="text"
|
|
32
|
-
id="register-lastName"
|
|
33
|
-
name="lastName"
|
|
34
|
-
autocomplete="family-name"
|
|
35
|
-
placeholder="Last name"
|
|
36
|
-
data-testid="register-lastName"
|
|
37
|
-
/>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<div class="form-group">
|
|
42
|
-
<label for="register-email">Email</label>
|
|
43
|
-
<input
|
|
44
|
-
type="email"
|
|
45
|
-
id="register-email"
|
|
46
|
-
name="email"
|
|
47
|
-
required
|
|
48
|
-
autocomplete="email"
|
|
49
|
-
placeholder="you@example.com"
|
|
50
|
-
data-testid="register-email"
|
|
51
|
-
/>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div class="form-group">
|
|
55
|
-
<label for="register-password">Password</label>
|
|
56
|
-
<input
|
|
57
|
-
type="password"
|
|
58
|
-
id="register-password"
|
|
59
|
-
name="password"
|
|
60
|
-
required
|
|
61
|
-
autocomplete="new-password"
|
|
62
|
-
placeholder="Choose a password"
|
|
63
|
-
minlength="8"
|
|
64
|
-
data-testid="register-password"
|
|
65
|
-
/>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<div class="form-group">
|
|
69
|
-
<label for="register-phone">Phone (optional)</label>
|
|
70
|
-
<input
|
|
71
|
-
type="tel"
|
|
72
|
-
id="register-phone"
|
|
73
|
-
name="phone"
|
|
74
|
-
autocomplete="tel"
|
|
75
|
-
placeholder="+44 7700 900000"
|
|
76
|
-
data-testid="register-phone"
|
|
77
|
-
/>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<button type="submit" class="btn btn-primary" data-testid="register-submit">
|
|
81
|
-
Create Account
|
|
82
|
-
</button>
|
|
83
|
-
</form>
|
|
13
|
+
<NymbalElement tag="nymbal-register-form" />
|
|
84
14
|
|
|
85
15
|
<p class="auth-footer">
|
|
86
16
|
Already have an account? <a href="/account/login">Sign in</a>
|
|
@@ -90,5 +20,9 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
|
90
20
|
</BaseLayout>
|
|
91
21
|
|
|
92
22
|
<script>
|
|
93
|
-
import '../../
|
|
23
|
+
import '../../lib/client.js'
|
|
24
|
+
import { NymbalRegisterForm } from '@nymbalhq/web-components/elements/nymbal-register-form'
|
|
25
|
+
import '../../scripts/signed-in-redirect.js'
|
|
26
|
+
|
|
27
|
+
NymbalRegisterForm.define()
|
|
94
28
|
</script>
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
import '
|
|
2
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
3
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
|
+
import { fetchCart } from '../lib/api'
|
|
5
|
+
|
|
6
|
+
const cart = await fetchCart(Astro.request.headers.get('cookie'))
|
|
4
7
|
---
|
|
5
8
|
|
|
6
|
-
<BaseLayout title="Your Cart | Nymbal" description="Review the items in your cart.">
|
|
7
|
-
<div class="container section"
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
<BaseLayout title="Your Cart | Nymbal" description="Review the items in your cart." cart={cart}>
|
|
10
|
+
<div class="container section" data-testid="cart-page">
|
|
11
|
+
<NymbalElement
|
|
12
|
+
tag="nymbal-cart-page"
|
|
13
|
+
attributes={cart === null ? {} : { cart: JSON.stringify(cart) }}
|
|
14
|
+
/>
|
|
11
15
|
</div>
|
|
12
16
|
</BaseLayout>
|
|
13
17
|
|
|
14
18
|
<script>
|
|
15
|
-
import '../
|
|
19
|
+
import '../lib/client.js'
|
|
20
|
+
import { NymbalCartPage } from '@nymbalhq/web-components/elements/nymbal-cart-page'
|
|
21
|
+
|
|
22
|
+
NymbalCartPage.define()
|
|
16
23
|
</script>
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../../styles/pages/plp.css'
|
|
3
|
-
import '../../styles/pages/home.css'
|
|
4
3
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import Pagination from '../../components/Pagination.astro'
|
|
4
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
5
|
+
import { cardImageOf, cardProductOf } from '@nymbalhq/web-components/ssr'
|
|
8
6
|
import { PRODUCT_SORT_LABELS } from '@nymbalhq/types'
|
|
9
7
|
import { fetchProducts, fetchCategories, fetchCapabilities, sortFromUrl } from '../../lib/api'
|
|
10
8
|
|
|
@@ -18,7 +16,10 @@ const [productResult, categories] = await Promise.all([
|
|
|
18
16
|
fetchCategories(),
|
|
19
17
|
])
|
|
20
18
|
|
|
21
|
-
const products = productResult.items
|
|
19
|
+
const products = productResult.items.map(cardProductOf)
|
|
20
|
+
// The first card's image is the page's LCP, and the preload scanner never reaches it inside the grid's shadow root:
|
|
21
|
+
// the head starts it.
|
|
22
|
+
const firstImage = products[0] === undefined ? null : cardImageOf(products[0]).url
|
|
22
23
|
const category = categories.find((c) => c.slug === slug)
|
|
23
24
|
const categoryName = category?.name ?? slug ?? 'Category'
|
|
24
25
|
const facets = productResult.facets ?? []
|
|
@@ -28,8 +29,9 @@ const facets = productResult.facets ?? []
|
|
|
28
29
|
title={`${categoryName} | Nymbal`}
|
|
29
30
|
description={category?.description ?? `Browse ${categoryName} products.`}
|
|
30
31
|
>
|
|
32
|
+
{firstImage && <link slot="head" rel="preload" as="image" href={firstImage} fetchpriority="high" />}
|
|
31
33
|
<div class="container section">
|
|
32
|
-
<
|
|
34
|
+
<NymbalElement tag="nymbal-breadcrumb" attributes={{ items: JSON.stringify([{ label: 'Home', href: '/' }, { label: categoryName }]) }} />
|
|
33
35
|
|
|
34
36
|
<div class="plp-header">
|
|
35
37
|
<h1 class="plp-title">{categoryName}</h1>
|
|
@@ -38,7 +40,6 @@ const facets = productResult.facets ?? []
|
|
|
38
40
|
{category.description}
|
|
39
41
|
</p>
|
|
40
42
|
)}
|
|
41
|
-
<p class="plp-count" id="product-count">{products.length} products</p>
|
|
42
43
|
</div>
|
|
43
44
|
|
|
44
45
|
<div class="plp-layout">
|
|
@@ -68,20 +69,15 @@ const facets = productResult.facets ?? []
|
|
|
68
69
|
</div>
|
|
69
70
|
)}
|
|
70
71
|
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<p class="plp-empty-text">Check back soon for new arrivals.</p>
|
|
81
|
-
</div>
|
|
82
|
-
)}
|
|
83
|
-
|
|
84
|
-
<Pagination />
|
|
72
|
+
<NymbalElement
|
|
73
|
+
tag="nymbal-product-grid"
|
|
74
|
+
attributes={{
|
|
75
|
+
products: JSON.stringify(products),
|
|
76
|
+
source: 'product-list',
|
|
77
|
+
...(productResult.nextCursor !== null && { 'has-more': '' }),
|
|
78
|
+
'data-category': slug!,
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
85
81
|
</div>
|
|
86
82
|
</div>
|
|
87
83
|
</div>
|