create-nymbal-app 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/bin/create-nymbal-app.mjs +9 -0
  2. package/dist/index.d.ts +37 -0
  3. package/dist/index.js +419 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +41 -7
  6. package/scaffold/project/astro/src/pages/index.astro +57 -0
  7. package/scaffold/templates/astro/README.md +26 -0
  8. package/scaffold/templates/astro/_dot.env.example +6 -0
  9. package/scaffold/templates/astro/_dot.gitignore +16 -0
  10. package/scaffold/templates/astro/_dot.node-version +1 -0
  11. package/scaffold/templates/astro/_dot.npmrc +1 -0
  12. package/scaffold/templates/astro/astro.config.mjs +14 -0
  13. package/scaffold/templates/astro/package.json +59 -0
  14. package/scaffold/templates/astro/src/components/AddressForm.astro +107 -0
  15. package/scaffold/templates/astro/src/components/Breadcrumb.astro +20 -0
  16. package/scaffold/templates/astro/src/components/CategoryNav.astro +27 -0
  17. package/scaffold/templates/astro/src/components/CheckoutSteps.astro +29 -0
  18. package/scaffold/templates/astro/src/components/Footer.astro +108 -0
  19. package/scaffold/templates/astro/src/components/Header.astro +192 -0
  20. package/scaffold/templates/astro/src/components/HeroSection.astro +17 -0
  21. package/scaffold/templates/astro/src/components/ImageGallery.astro +42 -0
  22. package/scaffold/templates/astro/src/components/NewsletterSignup.astro +39 -0
  23. package/scaffold/templates/astro/src/components/OrderSummary.astro +53 -0
  24. package/scaffold/templates/astro/src/components/Pagination.astro +13 -0
  25. package/scaffold/templates/astro/src/components/PriceDisplay.astro +25 -0
  26. package/scaffold/templates/astro/src/components/ProductCard.astro +44 -0
  27. package/scaffold/templates/astro/src/components/RelatedProducts.astro +28 -0
  28. package/scaffold/templates/astro/src/components/ReviewForm.astro +100 -0
  29. package/scaffold/templates/astro/src/components/ReviewList.astro +41 -0
  30. package/scaffold/templates/astro/src/components/SkipNav.astro +3 -0
  31. package/scaffold/templates/astro/src/components/SortDropdown.astro +26 -0
  32. package/scaffold/templates/astro/src/components/StockBadge.astro +32 -0
  33. package/scaffold/templates/astro/src/env.d.ts +2 -0
  34. package/scaffold/templates/astro/src/layouts/AccountLayout.astro +168 -0
  35. package/scaffold/templates/astro/src/layouts/BaseLayout.astro +55 -0
  36. package/scaffold/templates/astro/src/lib/api.ts +89 -0
  37. package/scaffold/templates/astro/src/lib/client.ts +13 -0
  38. package/scaffold/templates/astro/src/lib/format.ts +17 -0
  39. package/scaffold/templates/astro/src/lib/order-status.ts +14 -0
  40. package/scaffold/templates/astro/src/lib/seo.ts +62 -0
  41. package/scaffold/templates/astro/src/pages/404.astro +18 -0
  42. package/scaffold/templates/astro/src/pages/500.astro +17 -0
  43. package/scaffold/templates/astro/src/pages/account/addresses.astro +110 -0
  44. package/scaffold/templates/astro/src/pages/account/index.astro +67 -0
  45. package/scaffold/templates/astro/src/pages/account/login.astro +55 -0
  46. package/scaffold/templates/astro/src/pages/account/orders/[orderNumber].astro +108 -0
  47. package/scaffold/templates/astro/src/pages/account/orders.astro +81 -0
  48. package/scaffold/templates/astro/src/pages/account/profile.astro +131 -0
  49. package/scaffold/templates/astro/src/pages/account/register.astro +94 -0
  50. package/scaffold/templates/astro/src/pages/cart.astro +16 -0
  51. package/scaffold/templates/astro/src/pages/category/[slug].astro +73 -0
  52. package/scaffold/templates/astro/src/pages/checkout.astro +100 -0
  53. package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -0
  54. package/scaffold/templates/astro/src/pages/fragments/header.astro +5 -0
  55. package/scaffold/templates/astro/src/pages/fragments/nav.astro +9 -0
  56. package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +14 -0
  57. package/scaffold/templates/astro/src/pages/index.astro +49 -0
  58. package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +127 -0
  59. package/scaffold/templates/astro/src/pages/product/[slug].astro +107 -0
  60. package/scaffold/templates/astro/src/pages/products/index.astro +62 -0
  61. package/scaffold/templates/astro/src/pages/search.astro +37 -0
  62. package/scaffold/templates/astro/src/scripts/account-guard.ts +22 -0
  63. package/scaffold/templates/astro/src/scripts/auth-forms.ts +89 -0
  64. package/scaffold/templates/astro/src/scripts/cart-page.ts +143 -0
  65. package/scaffold/templates/astro/src/scripts/checkout.ts +277 -0
  66. package/scaffold/templates/astro/src/scripts/image-gallery.ts +22 -0
  67. package/scaffold/templates/astro/src/scripts/init-client.ts +16 -0
  68. package/scaffold/templates/astro/src/scripts/order-status.ts +27 -0
  69. package/scaffold/templates/astro/src/scripts/pdp.ts +67 -0
  70. package/scaffold/templates/astro/src/scripts/plp.ts +109 -0
  71. package/scaffold/templates/astro/src/scripts/search.ts +145 -0
  72. package/scaffold/templates/astro/src/styles/global.css +362 -0
  73. package/scaffold/templates/astro/src/styles/pages/account.css +331 -0
  74. package/scaffold/templates/astro/src/styles/pages/cart.css +243 -0
  75. package/scaffold/templates/astro/src/styles/pages/checkout.css +270 -0
  76. package/scaffold/templates/astro/src/styles/pages/confirmation.css +142 -0
  77. package/scaffold/templates/astro/src/styles/pages/error.css +69 -0
  78. package/scaffold/templates/astro/src/styles/pages/home.css +269 -0
  79. package/scaffold/templates/astro/src/styles/pages/pdp.css +272 -0
  80. package/scaffold/templates/astro/src/styles/pages/plp.css +213 -0
  81. package/scaffold/templates/astro/src/styles/pages/search.css +152 -0
  82. package/scaffold/templates/astro/tsconfig.json +3 -0
  83. package/scaffold/templates/nextjs/README.md +26 -0
  84. package/scaffold/templates/nextjs/_dot.env.example +6 -0
  85. package/scaffold/templates/nextjs/_dot.gitignore +17 -0
  86. package/scaffold/templates/nextjs/_dot.node-version +1 -0
  87. package/scaffold/templates/nextjs/_dot.npmrc +1 -0
  88. package/scaffold/templates/nextjs/app/account/addresses/page.tsx +10 -0
  89. package/scaffold/templates/nextjs/app/account/layout.tsx +57 -0
  90. package/scaffold/templates/nextjs/app/account/login/page.tsx +23 -0
  91. package/scaffold/templates/nextjs/app/account/orders/[orderNumber]/page.tsx +192 -0
  92. package/scaffold/templates/nextjs/app/account/orders/page.tsx +123 -0
  93. package/scaffold/templates/nextjs/app/account/page.tsx +59 -0
  94. package/scaffold/templates/nextjs/app/account/profile/page.tsx +10 -0
  95. package/scaffold/templates/nextjs/app/account/register/page.tsx +10 -0
  96. package/scaffold/templates/nextjs/app/cart/page.tsx +10 -0
  97. package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +58 -0
  98. package/scaffold/templates/nextjs/app/checkout/page.tsx +10 -0
  99. package/scaffold/templates/nextjs/app/error.tsx +49 -0
  100. package/scaffold/templates/nextjs/app/globals.css +473 -0
  101. package/scaffold/templates/nextjs/app/layout.tsx +29 -0
  102. package/scaffold/templates/nextjs/app/not-found.tsx +38 -0
  103. package/scaffold/templates/nextjs/app/order/[orderNumber]/page.tsx +127 -0
  104. package/scaffold/templates/nextjs/app/page.tsx +63 -0
  105. package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +111 -0
  106. package/scaffold/templates/nextjs/app/products/page.tsx +40 -0
  107. package/scaffold/templates/nextjs/app/search/page.tsx +24 -0
  108. package/scaffold/templates/nextjs/components/client/AddressManager.tsx +181 -0
  109. package/scaffold/templates/nextjs/components/client/AuthGuard.tsx +55 -0
  110. package/scaffold/templates/nextjs/components/client/CartDrawerToggle.tsx +39 -0
  111. package/scaffold/templates/nextjs/components/client/CartPage.tsx +120 -0
  112. package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +523 -0
  113. package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +68 -0
  114. package/scaffold/templates/nextjs/components/client/LoginForm.tsx +89 -0
  115. package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +12 -0
  116. package/scaffold/templates/nextjs/components/client/NewsletterSignup.tsx +74 -0
  117. package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +24 -0
  118. package/scaffold/templates/nextjs/components/client/OrderStatusRefresh.tsx +48 -0
  119. package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +148 -0
  120. package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +61 -0
  121. package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +112 -0
  122. package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +131 -0
  123. package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +116 -0
  124. package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +27 -0
  125. package/scaffold/templates/nextjs/components/client/SearchResults.tsx +141 -0
  126. package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +35 -0
  127. package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +36 -0
  128. package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +29 -0
  129. package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +60 -0
  130. package/scaffold/templates/nextjs/components/server/Footer.module.css +64 -0
  131. package/scaffold/templates/nextjs/components/server/Footer.tsx +79 -0
  132. package/scaffold/templates/nextjs/components/server/Header.module.css +147 -0
  133. package/scaffold/templates/nextjs/components/server/Header.tsx +102 -0
  134. package/scaffold/templates/nextjs/components/server/HeroSection.tsx +18 -0
  135. package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +79 -0
  136. package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +31 -0
  137. package/scaffold/templates/nextjs/components/server/ProductCard.tsx +59 -0
  138. package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +31 -0
  139. package/scaffold/templates/nextjs/components/server/ReviewList.tsx +62 -0
  140. package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -0
  141. package/scaffold/templates/nextjs/components/server/StockBadge.tsx +19 -0
  142. package/scaffold/templates/nextjs/lib/api.ts +89 -0
  143. package/scaffold/templates/nextjs/lib/client.ts +23 -0
  144. package/scaffold/templates/nextjs/lib/format.ts +7 -0
  145. package/scaffold/templates/nextjs/lib/order-status.ts +14 -0
  146. package/scaffold/templates/nextjs/lib/seo.ts +73 -0
  147. package/scaffold/templates/nextjs/next.config.mjs +9 -0
  148. package/scaffold/templates/nextjs/package.json +64 -0
  149. package/scaffold/templates/nextjs/styles/pages/account.module.css +377 -0
  150. package/scaffold/templates/nextjs/styles/pages/cart.module.css +240 -0
  151. package/scaffold/templates/nextjs/styles/pages/checkout.module.css +404 -0
  152. package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +237 -0
  153. package/scaffold/templates/nextjs/styles/pages/error.module.css +132 -0
  154. package/scaffold/templates/nextjs/styles/pages/home.module.css +285 -0
  155. package/scaffold/templates/nextjs/styles/pages/pdp.module.css +319 -0
  156. package/scaffold/templates/nextjs/styles/pages/plp.module.css +280 -0
  157. package/scaffold/templates/nextjs/styles/pages/search.module.css +192 -0
  158. package/scaffold/templates/nextjs/tsconfig.json +41 -0
  159. package/README.md +0 -4
@@ -0,0 +1,13 @@
1
+ import { createNymbalClient } from '@nymbalhq/sdk'
2
+ import { registerClient, registerComponents } from '@nymbalhq/web-components'
3
+
4
+ const client = createNymbalClient({
5
+ baseUrl: import.meta.env.PUBLIC_NYMBAL_API_URL ?? 'http://localhost:3001',
6
+ tokenStorage: 'localStorage',
7
+ })
8
+
9
+ registerClient(client)
10
+ registerComponents()
11
+ client.cart.load()
12
+
13
+ export { client }
@@ -0,0 +1,17 @@
1
+ export function formatPrice(minor: number, currency: string): string {
2
+ return new Intl.NumberFormat('en-GB', { style: 'currency', currency }).format(minor / 100)
3
+ }
4
+
5
+ export function formatDate(iso: string): string {
6
+ return new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium' }).format(new Date(iso))
7
+ }
8
+
9
+ /** Escapes text for interpolation into an HTML string; account pages render API data with innerHTML. */
10
+ export function escapeHtml(value: unknown): string {
11
+ return String(value ?? '')
12
+ .replace(/&/g, '&')
13
+ .replace(/</g, '&lt;')
14
+ .replace(/>/g, '&gt;')
15
+ .replace(/"/g, '&quot;')
16
+ .replace(/'/g, '&#39;')
17
+ }
@@ -0,0 +1,14 @@
1
+ import type { OrderStatus } from '@nymbalhq/types'
2
+
3
+ const PAID: ReadonlySet<OrderStatus> = new Set(['confirmed', 'processing', 'shipped', 'delivered'])
4
+
5
+ /** The order page's heading, from the order's real state: never "confirmed" before the payment is. */
6
+ export function orderHeading(status: OrderStatus): string {
7
+ if (PAID.has(status)) return 'Order confirmed'
8
+ if (status === 'pending') return 'Awaiting payment'
9
+ return `Order ${status.replace(/_/g, ' ')}`
10
+ }
11
+
12
+ export function isPaid(status: OrderStatus): boolean {
13
+ return PAID.has(status)
14
+ }
@@ -0,0 +1,62 @@
1
+ import type { DenormalisedProduct } from '@nymbalhq/sdk'
2
+
3
+ export function productJsonLd(product: DenormalisedProduct, baseUrl: string) {
4
+ // The SDK types `media` as unknown: the projection stores either URLs or `{ url }` objects.
5
+ const media: unknown[] = Array.isArray(product.media) ? product.media : []
6
+ const images = media
7
+ .map((m) =>
8
+ typeof m === 'string'
9
+ ? m
10
+ : m && typeof m === 'object' && 'url' in m && typeof m.url === 'string'
11
+ ? m.url
12
+ : null,
13
+ )
14
+ .filter((url): url is string => url !== null)
15
+
16
+ const sku = product.variants?.[0]?.sku ?? product.id
17
+
18
+ const lowPrice = (product.priceRange?.minMinor ?? product.priceMinor ?? 0) / 100
19
+ const highPrice = (product.priceRange?.maxMinor ?? product.priceMinor ?? 0) / 100
20
+
21
+ return {
22
+ '@context': 'https://schema.org',
23
+ '@type': 'Product',
24
+ name: product.name,
25
+ description: product.description,
26
+ image: images,
27
+ sku,
28
+ url: `${baseUrl}/product/${product.slug}`,
29
+ offers: {
30
+ '@type': 'AggregateOffer',
31
+ lowPrice: lowPrice.toFixed(2),
32
+ highPrice: highPrice.toFixed(2),
33
+ priceCurrency: product.currency,
34
+ availability: product.inStock
35
+ ? 'https://schema.org/InStock'
36
+ : 'https://schema.org/OutOfStock',
37
+ offerCount: product.variants?.length ?? 1,
38
+ },
39
+ }
40
+ }
41
+
42
+ export function breadcrumbJsonLd(items: Array<{ name: string; url: string }>) {
43
+ return {
44
+ '@context': 'https://schema.org',
45
+ '@type': 'BreadcrumbList',
46
+ itemListElement: items.map((item, index) => ({
47
+ '@type': 'ListItem',
48
+ position: index + 1,
49
+ name: item.name,
50
+ item: item.url,
51
+ })),
52
+ }
53
+ }
54
+
55
+ export function organizationJsonLd(storeName: string, url: string) {
56
+ return {
57
+ '@context': 'https://schema.org',
58
+ '@type': 'Organization',
59
+ name: storeName,
60
+ url,
61
+ }
62
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ import '../styles/pages/error.css'
3
+ import BaseLayout from '../layouts/BaseLayout.astro'
4
+ ---
5
+
6
+ <BaseLayout title="Page Not Found | Nymbal" description="The page you're looking for doesn't exist.">
7
+ <div class="error-page">
8
+ <span class="error-code">404</span>
9
+ <h1 class="error-title">Page not found</h1>
10
+ <p class="error-message">
11
+ The page you're looking for doesn't exist or may have been moved.
12
+ </p>
13
+ <div class="error-actions">
14
+ <a href="/" class="btn btn-primary">Go to Homepage</a>
15
+ <a href="/products" class="btn btn-secondary">Browse Products</a>
16
+ </div>
17
+ </div>
18
+ </BaseLayout>
@@ -0,0 +1,17 @@
1
+ ---
2
+ import '../styles/pages/error.css'
3
+ import BaseLayout from '../layouts/BaseLayout.astro'
4
+ ---
5
+
6
+ <BaseLayout title="Server Error | Nymbal" description="Something went wrong on our end.">
7
+ <div class="error-page server-error">
8
+ <span class="error-code">500</span>
9
+ <h1 class="error-title">Something went wrong</h1>
10
+ <p class="error-message">
11
+ We're having trouble loading this page. Please try again in a moment.
12
+ </p>
13
+ <div class="error-actions">
14
+ <a href="/" class="btn btn-primary">Go to Homepage</a>
15
+ </div>
16
+ </div>
17
+ </BaseLayout>
@@ -0,0 +1,110 @@
1
+ ---
2
+ import '../../styles/pages/account.css'
3
+ import '../../styles/pages/checkout.css'
4
+ import AccountLayout from '../../layouts/AccountLayout.astro'
5
+ import AddressForm from '../../components/AddressForm.astro'
6
+ ---
7
+
8
+ <AccountLayout title="Addresses">
9
+ <h1 class="account-page-title">Addresses</h1>
10
+
11
+ <div id="addresses-container" data-testid="addresses-list">
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>
27
+ </AccountLayout>
28
+
29
+ <script>
30
+ import type { Address, CustomerAddress } from '@nymbalhq/types'
31
+ import { client } from '../../lib/client.js'
32
+ import { escapeHtml } from '../../lib/format.js'
33
+
34
+ const container = document.getElementById('addresses-container')
35
+ const form = document.getElementById('add-address-form') as HTMLFormElement | null
36
+ const messageEl = document.getElementById('address-form-message')
37
+
38
+ function renderAddress(addr: CustomerAddress): string {
39
+ const isDefault = addr.isDefaultShipping || addr.isDefaultBilling
40
+ return `
41
+ <div class="address-card ${isDefault ? 'default' : ''}" data-testid="address-${escapeHtml(addr.id)}">
42
+ ${isDefault ? '<span class="address-card-badge">Default</span>' : ''}
43
+ <div class="address-card-line">${escapeHtml(`${addr.firstName} ${addr.lastName}`)}</div>
44
+ <div class="address-card-line">${escapeHtml(addr.addressLine1)}</div>
45
+ ${addr.addressLine2 ? `<div class="address-card-line">${escapeHtml(addr.addressLine2)}</div>` : ''}
46
+ <div class="address-card-line">${escapeHtml(`${addr.city}, ${addr.region} ${addr.postalCode}`)}</div>
47
+ <div class="address-card-line">${escapeHtml(addr.country)}</div>
48
+ </div>
49
+ `
50
+ }
51
+
52
+ async function loadAddresses() {
53
+ if (!container) return
54
+
55
+ try {
56
+ const addresses = await client.customer.addresses.list()
57
+
58
+ if (addresses.length === 0) {
59
+ container.innerHTML = `
60
+ <div class="account-empty">
61
+ <h2 class="account-empty-title">No saved addresses</h2>
62
+ <p class="account-empty-text">Add an address below to get started.</p>
63
+ </div>
64
+ `
65
+ return
66
+ }
67
+
68
+ container.innerHTML = `<div class="address-list">${addresses.map(renderAddress).join('')}</div>`
69
+ } catch {
70
+ container.innerHTML = `
71
+ <div class="account-empty">
72
+ <p class="account-empty-text">Unable to load addresses.</p>
73
+ </div>
74
+ `
75
+ }
76
+ }
77
+
78
+ form?.addEventListener('submit', async (e) => {
79
+ e.preventDefault()
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
+ })
108
+
109
+ loadAddresses()
110
+ </script>
@@ -0,0 +1,67 @@
1
+ ---
2
+ import '../../styles/pages/account.css'
3
+ import AccountLayout from '../../layouts/AccountLayout.astro'
4
+ ---
5
+
6
+ <AccountLayout title="My Account">
7
+ <h1 class="account-page-title">Dashboard</h1>
8
+
9
+ <div id="account-dashboard" data-testid="account-dashboard">
10
+ <div class="account-stats">
11
+ <div class="account-stat-card">
12
+ <div class="account-stat-label">Welcome back</div>
13
+ <div class="account-stat-value" id="dashboard-name">--</div>
14
+ </div>
15
+ <div class="account-stat-card">
16
+ <div class="account-stat-label">Email</div>
17
+ <div class="account-stat-value" id="dashboard-email" style="font-size: var(--nymbal-font-size-sm, 0.875rem);">--</div>
18
+ </div>
19
+ <div class="account-stat-card">
20
+ <div class="account-stat-label">Member since</div>
21
+ <div class="account-stat-value" id="dashboard-since" style="font-size: var(--nymbal-font-size-sm, 0.875rem);">--</div>
22
+ </div>
23
+ </div>
24
+
25
+ <h2 class="section-title">Quick Links</h2>
26
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--nymbal-spacing-md, 1rem);">
27
+ <a href="/account/orders" class="category-card" style="min-height: auto; padding: var(--nymbal-spacing-md, 1rem);">
28
+ <span class="category-card-name">Order History</span>
29
+ </a>
30
+ <a href="/account/addresses" class="category-card" style="min-height: auto; padding: var(--nymbal-spacing-md, 1rem);">
31
+ <span class="category-card-name">Addresses</span>
32
+ </a>
33
+ <a href="/account/profile" class="category-card" style="min-height: auto; padding: var(--nymbal-spacing-md, 1rem);">
34
+ <span class="category-card-name">Profile Settings</span>
35
+ </a>
36
+ <a href="/products" class="category-card" style="min-height: auto; padding: var(--nymbal-spacing-md, 1rem);">
37
+ <span class="category-card-name">Continue Shopping</span>
38
+ </a>
39
+ </div>
40
+ </div>
41
+ </AccountLayout>
42
+
43
+ <script>
44
+ import { client } from '../../lib/client.js'
45
+ import { formatDate } from '../../lib/format.js'
46
+
47
+ function renderDashboard() {
48
+ const profile = client.auth.getState().customer
49
+
50
+ const nameEl = document.getElementById('dashboard-name')
51
+ const emailEl = document.getElementById('dashboard-email')
52
+ const sinceEl = document.getElementById('dashboard-since')
53
+
54
+ if (nameEl && profile) {
55
+ nameEl.textContent = [profile.firstName, profile.lastName].filter(Boolean).join(' ') || 'User'
56
+ }
57
+ if (emailEl && profile) {
58
+ emailEl.textContent = profile.email || '--'
59
+ }
60
+ if (sinceEl && profile?.createdAt) {
61
+ sinceEl.textContent = formatDate(profile.createdAt)
62
+ }
63
+ }
64
+
65
+ client.auth.subscribe(renderDashboard)
66
+ renderDashboard()
67
+ </script>
@@ -0,0 +1,55 @@
1
+ ---
2
+ import '../../styles/pages/account.css'
3
+ import BaseLayout from '../../layouts/BaseLayout.astro'
4
+ ---
5
+
6
+ <BaseLayout title="Sign In | Nymbal" description="Sign in to your Nymbal account.">
7
+ <div class="auth-page">
8
+ <div class="auth-card">
9
+ <h1 class="auth-card-title">Sign In</h1>
10
+ <p class="auth-card-subtitle">Welcome back. Enter your credentials to continue.</p>
11
+
12
+ <div class="checkout-error" id="login-error" style="display:none; margin-bottom: var(--nymbal-spacing-md, 1rem);" role="alert"></div>
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>
45
+
46
+ <p class="auth-footer">
47
+ Don't have an account? <a href="/account/register">Create one</a>
48
+ </p>
49
+ </div>
50
+ </div>
51
+ </BaseLayout>
52
+
53
+ <script>
54
+ import '../../scripts/auth-forms.js'
55
+ </script>
@@ -0,0 +1,108 @@
1
+ ---
2
+ import '../../../styles/pages/account.css'
3
+ import '../../../styles/pages/confirmation.css'
4
+ import AccountLayout from '../../../layouts/AccountLayout.astro'
5
+ ---
6
+
7
+ <AccountLayout title="Order Detail">
8
+ <div id="order-detail-container" data-testid="order-detail" data-order-number={Astro.params.orderNumber}>
9
+ <p style="color: var(--nymbal-color-text-muted, #71717a); font-size: var(--nymbal-font-size-sm, 0.875rem);">
10
+ Loading order details...
11
+ </p>
12
+ </div>
13
+ </AccountLayout>
14
+
15
+ <script>
16
+ import { client } from '../../../lib/client.js'
17
+ import { escapeHtml, formatPrice, formatDate } from '../../../lib/format.js'
18
+
19
+ const container = document.getElementById('order-detail-container')
20
+ const orderNumber = container?.dataset.orderNumber
21
+
22
+ async function loadOrderDetail() {
23
+ if (!container || !orderNumber) return
24
+
25
+ try {
26
+ const order = await client.customer.orders.get(orderNumber)
27
+ const ship = order.shippingAddress
28
+
29
+ container.innerHTML = `
30
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--nymbal-spacing-lg, 1.5rem); flex-wrap: wrap; gap: var(--nymbal-spacing-sm, 0.5rem);">
31
+ <div>
32
+ <h1 class="account-page-title" style="margin-bottom: 0;">Order #${escapeHtml(order.orderNumber)}</h1>
33
+ ${order.createdAt ? `<p class="account-page-subtitle" style="margin-top: var(--nymbal-spacing-xs, 0.25rem);">${formatDate(order.createdAt)}</p>` : ''}
34
+ </div>
35
+ <span class="order-status status-${escapeHtml(order.status)}">${escapeHtml(order.status)}</span>
36
+ </div>
37
+
38
+ <div class="confirmation-card">
39
+ <h2 class="confirmation-card-title">Items</h2>
40
+ <div class="confirmation-items" style="margin-top: 0; padding-top: 0; border-top: none;">
41
+ ${(order.lineItems ?? [])
42
+ .map(
43
+ (item) => `
44
+ <div class="confirmation-item">
45
+ <span>
46
+ <span class="confirmation-item-name">${escapeHtml(item.productName)}</span>
47
+ ${item.variantName ? ` - ${escapeHtml(item.variantName)}` : ''}
48
+ <span class="confirmation-item-qty"> x${item.qty}</span>
49
+ </span>
50
+ <span class="confirmation-item-price">${formatPrice(item.lineTotalMinor, order.currency)}</span>
51
+ </div>
52
+ `,
53
+ )
54
+ .join('')}
55
+ </div>
56
+
57
+ <div class="confirmation-totals">
58
+ <div class="confirmation-total-row">
59
+ <span>Subtotal</span>
60
+ <span>${formatPrice(order.subtotalMinor, order.currency)}</span>
61
+ </div>
62
+ ${order.shippingTotalMinor != null ? `
63
+ <div class="confirmation-total-row">
64
+ <span>Shipping</span>
65
+ <span>${order.shippingTotalMinor === 0 ? 'Free' : formatPrice(order.shippingTotalMinor, order.currency)}</span>
66
+ </div>
67
+ ` : ''}
68
+ ${order.taxTotalMinor != null ? `
69
+ <div class="confirmation-total-row">
70
+ <span>Tax</span>
71
+ <span>${formatPrice(order.taxTotalMinor, order.currency)}</span>
72
+ </div>
73
+ ` : ''}
74
+ <div class="confirmation-total-row grand-total">
75
+ <span>Total</span>
76
+ <span>${formatPrice(order.totalMinor, order.currency)}</span>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ ${ship ? `
82
+ <div class="confirmation-card">
83
+ <h2 class="confirmation-card-title">Shipping Address</h2>
84
+ <p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a); line-height: 1.6;">
85
+ ${escapeHtml(`${ship.firstName} ${ship.lastName}`)}<br/>
86
+ ${escapeHtml(ship.addressLine1)}<br/>
87
+ ${ship.addressLine2 ? escapeHtml(ship.addressLine2) + '<br/>' : ''}
88
+ ${escapeHtml(`${ship.city}, ${ship.region} ${ship.postalCode}`)}<br/>
89
+ ${escapeHtml(ship.country)}
90
+ </p>
91
+ </div>
92
+ ` : ''}
93
+
94
+ <a href="/account/orders" class="btn btn-secondary" style="margin-top: var(--nymbal-spacing-md, 1rem);">Back to Orders</a>
95
+ `
96
+ } catch {
97
+ container.innerHTML = `
98
+ <div class="account-empty">
99
+ <h2 class="account-empty-title">Unable to load order</h2>
100
+ <p class="account-empty-text">Please try again later.</p>
101
+ <a href="/account/orders" class="btn btn-secondary">Back to Orders</a>
102
+ </div>
103
+ `
104
+ }
105
+ }
106
+
107
+ loadOrderDetail()
108
+ </script>
@@ -0,0 +1,81 @@
1
+ ---
2
+ import '../../styles/pages/account.css'
3
+ import AccountLayout from '../../layouts/AccountLayout.astro'
4
+ ---
5
+
6
+ <AccountLayout title="Order History">
7
+ <h1 class="account-page-title">Order History</h1>
8
+
9
+ <div id="orders-container" data-testid="orders-list">
10
+ <p style="color: var(--nymbal-color-text-muted, #71717a); font-size: var(--nymbal-font-size-sm, 0.875rem);">
11
+ Loading orders...
12
+ </p>
13
+ </div>
14
+ </AccountLayout>
15
+
16
+ <script>
17
+ import type { Order } from '@nymbalhq/types'
18
+ import { client } from '../../lib/client.js'
19
+ import { escapeHtml, formatPrice, formatDate } from '../../lib/format.js'
20
+
21
+ const container = document.getElementById('orders-container')
22
+
23
+ function renderOrder(order: Order): string {
24
+ const number = escapeHtml(order.orderNumber)
25
+ const items = order.lineItems ?? []
26
+ return `
27
+ <a class="order-card" href="/account/orders/${encodeURIComponent(order.orderNumber)}" data-testid="order-${number}">
28
+ <div class="order-card-header">
29
+ <div>
30
+ <span class="order-card-number">#${number}</span>
31
+ ${order.createdAt ? `<span class="order-card-date" style="margin-left: var(--nymbal-spacing-sm, 0.5rem);">${formatDate(order.createdAt)}</span>` : ''}
32
+ </div>
33
+ <span class="order-status status-${escapeHtml(order.status)}">${escapeHtml(order.status)}</span>
34
+ </div>
35
+ <div class="order-card-body">
36
+ <div class="order-card-items">
37
+ ${items
38
+ .slice(0, 3)
39
+ .map((item) => `<span>${escapeHtml(item.productName)} x${item.qty}</span>`)
40
+ .join('')}
41
+ ${items.length > 3 ? `<span>+${items.length - 3} more items</span>` : ''}
42
+ </div>
43
+ </div>
44
+ <div class="order-card-footer">
45
+ <span class="order-card-total">${formatPrice(order.totalMinor, order.currency)}</span>
46
+ <span class="order-card-link">View Details</span>
47
+ </div>
48
+ </a>
49
+ `
50
+ }
51
+
52
+ async function loadOrders() {
53
+ if (!container) return
54
+
55
+ try {
56
+ const { items: orders } = await client.customer.orders.list()
57
+
58
+ if (orders.length === 0) {
59
+ container.innerHTML = `
60
+ <div class="account-empty">
61
+ <h2 class="account-empty-title">No orders yet</h2>
62
+ <p class="account-empty-text">Once you place an order, it will appear here.</p>
63
+ <a href="/products" class="btn btn-primary">Start Shopping</a>
64
+ </div>
65
+ `
66
+ return
67
+ }
68
+
69
+ container.innerHTML = `<div class="order-list">${orders.map(renderOrder).join('')}</div>`
70
+ } catch {
71
+ container.innerHTML = `
72
+ <div class="account-empty">
73
+ <h2 class="account-empty-title">Unable to load orders</h2>
74
+ <p class="account-empty-text">Please try again later.</p>
75
+ </div>
76
+ `
77
+ }
78
+ }
79
+
80
+ loadOrders()
81
+ </script>