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.
- package/bin/create-nymbal-app.mjs +9 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +419 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -7
- package/scaffold/project/astro/src/pages/index.astro +57 -0
- package/scaffold/templates/astro/README.md +26 -0
- package/scaffold/templates/astro/_dot.env.example +6 -0
- package/scaffold/templates/astro/_dot.gitignore +16 -0
- package/scaffold/templates/astro/_dot.node-version +1 -0
- package/scaffold/templates/astro/_dot.npmrc +1 -0
- package/scaffold/templates/astro/astro.config.mjs +14 -0
- package/scaffold/templates/astro/package.json +59 -0
- package/scaffold/templates/astro/src/components/AddressForm.astro +107 -0
- package/scaffold/templates/astro/src/components/Breadcrumb.astro +20 -0
- package/scaffold/templates/astro/src/components/CategoryNav.astro +27 -0
- package/scaffold/templates/astro/src/components/CheckoutSteps.astro +29 -0
- package/scaffold/templates/astro/src/components/Footer.astro +108 -0
- package/scaffold/templates/astro/src/components/Header.astro +192 -0
- package/scaffold/templates/astro/src/components/HeroSection.astro +17 -0
- package/scaffold/templates/astro/src/components/ImageGallery.astro +42 -0
- package/scaffold/templates/astro/src/components/NewsletterSignup.astro +39 -0
- package/scaffold/templates/astro/src/components/OrderSummary.astro +53 -0
- package/scaffold/templates/astro/src/components/Pagination.astro +13 -0
- package/scaffold/templates/astro/src/components/PriceDisplay.astro +25 -0
- package/scaffold/templates/astro/src/components/ProductCard.astro +44 -0
- package/scaffold/templates/astro/src/components/RelatedProducts.astro +28 -0
- package/scaffold/templates/astro/src/components/ReviewForm.astro +100 -0
- package/scaffold/templates/astro/src/components/ReviewList.astro +41 -0
- package/scaffold/templates/astro/src/components/SkipNav.astro +3 -0
- package/scaffold/templates/astro/src/components/SortDropdown.astro +26 -0
- package/scaffold/templates/astro/src/components/StockBadge.astro +32 -0
- package/scaffold/templates/astro/src/env.d.ts +2 -0
- package/scaffold/templates/astro/src/layouts/AccountLayout.astro +168 -0
- package/scaffold/templates/astro/src/layouts/BaseLayout.astro +55 -0
- package/scaffold/templates/astro/src/lib/api.ts +89 -0
- package/scaffold/templates/astro/src/lib/client.ts +13 -0
- package/scaffold/templates/astro/src/lib/format.ts +17 -0
- package/scaffold/templates/astro/src/lib/order-status.ts +14 -0
- package/scaffold/templates/astro/src/lib/seo.ts +62 -0
- package/scaffold/templates/astro/src/pages/404.astro +18 -0
- package/scaffold/templates/astro/src/pages/500.astro +17 -0
- package/scaffold/templates/astro/src/pages/account/addresses.astro +110 -0
- package/scaffold/templates/astro/src/pages/account/index.astro +67 -0
- package/scaffold/templates/astro/src/pages/account/login.astro +55 -0
- package/scaffold/templates/astro/src/pages/account/orders/[orderNumber].astro +108 -0
- package/scaffold/templates/astro/src/pages/account/orders.astro +81 -0
- package/scaffold/templates/astro/src/pages/account/profile.astro +131 -0
- package/scaffold/templates/astro/src/pages/account/register.astro +94 -0
- package/scaffold/templates/astro/src/pages/cart.astro +16 -0
- package/scaffold/templates/astro/src/pages/category/[slug].astro +73 -0
- package/scaffold/templates/astro/src/pages/checkout.astro +100 -0
- package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -0
- package/scaffold/templates/astro/src/pages/fragments/header.astro +5 -0
- package/scaffold/templates/astro/src/pages/fragments/nav.astro +9 -0
- package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +14 -0
- package/scaffold/templates/astro/src/pages/index.astro +49 -0
- package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +127 -0
- package/scaffold/templates/astro/src/pages/product/[slug].astro +107 -0
- package/scaffold/templates/astro/src/pages/products/index.astro +62 -0
- package/scaffold/templates/astro/src/pages/search.astro +37 -0
- package/scaffold/templates/astro/src/scripts/account-guard.ts +22 -0
- package/scaffold/templates/astro/src/scripts/auth-forms.ts +89 -0
- package/scaffold/templates/astro/src/scripts/cart-page.ts +143 -0
- package/scaffold/templates/astro/src/scripts/checkout.ts +277 -0
- package/scaffold/templates/astro/src/scripts/image-gallery.ts +22 -0
- package/scaffold/templates/astro/src/scripts/init-client.ts +16 -0
- package/scaffold/templates/astro/src/scripts/order-status.ts +27 -0
- package/scaffold/templates/astro/src/scripts/pdp.ts +67 -0
- package/scaffold/templates/astro/src/scripts/plp.ts +109 -0
- package/scaffold/templates/astro/src/scripts/search.ts +145 -0
- package/scaffold/templates/astro/src/styles/global.css +362 -0
- package/scaffold/templates/astro/src/styles/pages/account.css +331 -0
- package/scaffold/templates/astro/src/styles/pages/cart.css +243 -0
- package/scaffold/templates/astro/src/styles/pages/checkout.css +270 -0
- package/scaffold/templates/astro/src/styles/pages/confirmation.css +142 -0
- package/scaffold/templates/astro/src/styles/pages/error.css +69 -0
- package/scaffold/templates/astro/src/styles/pages/home.css +269 -0
- package/scaffold/templates/astro/src/styles/pages/pdp.css +272 -0
- package/scaffold/templates/astro/src/styles/pages/plp.css +213 -0
- package/scaffold/templates/astro/src/styles/pages/search.css +152 -0
- package/scaffold/templates/astro/tsconfig.json +3 -0
- package/scaffold/templates/nextjs/README.md +26 -0
- package/scaffold/templates/nextjs/_dot.env.example +6 -0
- package/scaffold/templates/nextjs/_dot.gitignore +17 -0
- package/scaffold/templates/nextjs/_dot.node-version +1 -0
- package/scaffold/templates/nextjs/_dot.npmrc +1 -0
- package/scaffold/templates/nextjs/app/account/addresses/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/account/layout.tsx +57 -0
- package/scaffold/templates/nextjs/app/account/login/page.tsx +23 -0
- package/scaffold/templates/nextjs/app/account/orders/[orderNumber]/page.tsx +192 -0
- package/scaffold/templates/nextjs/app/account/orders/page.tsx +123 -0
- package/scaffold/templates/nextjs/app/account/page.tsx +59 -0
- package/scaffold/templates/nextjs/app/account/profile/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/account/register/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/cart/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +58 -0
- package/scaffold/templates/nextjs/app/checkout/page.tsx +10 -0
- package/scaffold/templates/nextjs/app/error.tsx +49 -0
- package/scaffold/templates/nextjs/app/globals.css +473 -0
- package/scaffold/templates/nextjs/app/layout.tsx +29 -0
- package/scaffold/templates/nextjs/app/not-found.tsx +38 -0
- package/scaffold/templates/nextjs/app/order/[orderNumber]/page.tsx +127 -0
- package/scaffold/templates/nextjs/app/page.tsx +63 -0
- package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +111 -0
- package/scaffold/templates/nextjs/app/products/page.tsx +40 -0
- package/scaffold/templates/nextjs/app/search/page.tsx +24 -0
- package/scaffold/templates/nextjs/components/client/AddressManager.tsx +181 -0
- package/scaffold/templates/nextjs/components/client/AuthGuard.tsx +55 -0
- package/scaffold/templates/nextjs/components/client/CartDrawerToggle.tsx +39 -0
- package/scaffold/templates/nextjs/components/client/CartPage.tsx +120 -0
- package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +523 -0
- package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +68 -0
- package/scaffold/templates/nextjs/components/client/LoginForm.tsx +89 -0
- package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +12 -0
- package/scaffold/templates/nextjs/components/client/NewsletterSignup.tsx +74 -0
- package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +24 -0
- package/scaffold/templates/nextjs/components/client/OrderStatusRefresh.tsx +48 -0
- package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +148 -0
- package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +61 -0
- package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +112 -0
- package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +131 -0
- package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +116 -0
- package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +27 -0
- package/scaffold/templates/nextjs/components/client/SearchResults.tsx +141 -0
- package/scaffold/templates/nextjs/components/client/SortDropdown.tsx +35 -0
- package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +36 -0
- package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +29 -0
- package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +60 -0
- package/scaffold/templates/nextjs/components/server/Footer.module.css +64 -0
- package/scaffold/templates/nextjs/components/server/Footer.tsx +79 -0
- package/scaffold/templates/nextjs/components/server/Header.module.css +147 -0
- package/scaffold/templates/nextjs/components/server/Header.tsx +102 -0
- package/scaffold/templates/nextjs/components/server/HeroSection.tsx +18 -0
- package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +79 -0
- package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +31 -0
- package/scaffold/templates/nextjs/components/server/ProductCard.tsx +59 -0
- package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +31 -0
- package/scaffold/templates/nextjs/components/server/ReviewList.tsx +62 -0
- package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -0
- package/scaffold/templates/nextjs/components/server/StockBadge.tsx +19 -0
- package/scaffold/templates/nextjs/lib/api.ts +89 -0
- package/scaffold/templates/nextjs/lib/client.ts +23 -0
- package/scaffold/templates/nextjs/lib/format.ts +7 -0
- package/scaffold/templates/nextjs/lib/order-status.ts +14 -0
- package/scaffold/templates/nextjs/lib/seo.ts +73 -0
- package/scaffold/templates/nextjs/next.config.mjs +9 -0
- package/scaffold/templates/nextjs/package.json +64 -0
- package/scaffold/templates/nextjs/styles/pages/account.module.css +377 -0
- package/scaffold/templates/nextjs/styles/pages/cart.module.css +240 -0
- package/scaffold/templates/nextjs/styles/pages/checkout.module.css +404 -0
- package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +237 -0
- package/scaffold/templates/nextjs/styles/pages/error.module.css +132 -0
- package/scaffold/templates/nextjs/styles/pages/home.module.css +285 -0
- package/scaffold/templates/nextjs/styles/pages/pdp.module.css +319 -0
- package/scaffold/templates/nextjs/styles/pages/plp.module.css +280 -0
- package/scaffold/templates/nextjs/styles/pages/search.module.css +192 -0
- package/scaffold/templates/nextjs/tsconfig.json +41 -0
- package/README.md +0 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../../styles/pages/account.css'
|
|
3
|
+
import AccountLayout from '../../layouts/AccountLayout.astro'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<AccountLayout title="Profile">
|
|
7
|
+
<h1 class="account-page-title">Profile</h1>
|
|
8
|
+
<p class="account-page-subtitle">Update your personal information.</p>
|
|
9
|
+
|
|
10
|
+
<form id="profile-form" class="account-form" data-testid="profile-form">
|
|
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>
|
|
65
|
+
</AccountLayout>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
import { client } from '../../lib/client.js'
|
|
69
|
+
|
|
70
|
+
const form = document.getElementById('profile-form') as HTMLFormElement | null
|
|
71
|
+
const messageEl = document.getElementById('profile-message')
|
|
72
|
+
|
|
73
|
+
function populateForm() {
|
|
74
|
+
const profile = client.auth.getState().customer
|
|
75
|
+
if (!profile) return
|
|
76
|
+
|
|
77
|
+
const firstNameEl = document.getElementById('profile-firstName') as HTMLInputElement
|
|
78
|
+
const lastNameEl = document.getElementById('profile-lastName') as HTMLInputElement
|
|
79
|
+
const emailEl = document.getElementById('profile-email') as HTMLInputElement
|
|
80
|
+
const phoneEl = document.getElementById('profile-phone') as HTMLInputElement
|
|
81
|
+
|
|
82
|
+
if (firstNameEl) firstNameEl.value = profile.firstName ?? ''
|
|
83
|
+
if (lastNameEl) lastNameEl.value = profile.lastName ?? ''
|
|
84
|
+
if (emailEl) emailEl.value = profile.email ?? ''
|
|
85
|
+
if (phoneEl) phoneEl.value = profile.phone ?? ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Load profile data
|
|
89
|
+
async function init() {
|
|
90
|
+
try {
|
|
91
|
+
await client.auth.loadProfile()
|
|
92
|
+
} catch {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
populateForm()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
client.auth.subscribe(populateForm)
|
|
99
|
+
init()
|
|
100
|
+
|
|
101
|
+
form?.addEventListener('submit', async (e) => {
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
if (!form || !messageEl) return
|
|
104
|
+
|
|
105
|
+
const firstName = (document.getElementById('profile-firstName') as HTMLInputElement)?.value
|
|
106
|
+
const lastName = (document.getElementById('profile-lastName') as HTMLInputElement)?.value
|
|
107
|
+
const phone = (document.getElementById('profile-phone') as HTMLInputElement)?.value
|
|
108
|
+
|
|
109
|
+
const submitBtn = form.querySelector('[data-testid="profile-save"]') as HTMLButtonElement
|
|
110
|
+
if (submitBtn) {
|
|
111
|
+
submitBtn.disabled = true
|
|
112
|
+
submitBtn.textContent = 'Saving...'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await client.auth.updateProfile({ firstName, lastName, phone })
|
|
117
|
+
messageEl.textContent = 'Profile updated successfully.'
|
|
118
|
+
messageEl.style.color = 'var(--nymbal-color-success, #16a34a)'
|
|
119
|
+
messageEl.style.display = 'block'
|
|
120
|
+
} catch (err) {
|
|
121
|
+
messageEl.textContent = err instanceof Error ? err.message : 'Failed to update profile.'
|
|
122
|
+
messageEl.style.color = 'var(--nymbal-color-error, #dc2626)'
|
|
123
|
+
messageEl.style.display = 'block'
|
|
124
|
+
} finally {
|
|
125
|
+
if (submitBtn) {
|
|
126
|
+
submitBtn.disabled = false
|
|
127
|
+
submitBtn.textContent = 'Save Changes'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
</script>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../../styles/pages/account.css'
|
|
3
|
+
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<BaseLayout title="Create Account | Nymbal" description="Create a new Nymbal account.">
|
|
7
|
+
<div class="auth-page">
|
|
8
|
+
<div class="auth-card">
|
|
9
|
+
<h1 class="auth-card-title">Create Account</h1>
|
|
10
|
+
<p class="auth-card-subtitle">Join us and start shopping.</p>
|
|
11
|
+
|
|
12
|
+
<div class="checkout-error" id="register-error" style="display:none; margin-bottom: var(--nymbal-spacing-md, 1rem);" role="alert"></div>
|
|
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>
|
|
84
|
+
|
|
85
|
+
<p class="auth-footer">
|
|
86
|
+
Already have an account? <a href="/account/login">Sign in</a>
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</BaseLayout>
|
|
91
|
+
|
|
92
|
+
<script>
|
|
93
|
+
import '../../scripts/auth-forms.js'
|
|
94
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../styles/pages/cart.css'
|
|
3
|
+
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<BaseLayout title="Your Cart | Nymbal" description="Review the items in your cart.">
|
|
7
|
+
<div class="container section" id="cart-content" data-testid="cart-page">
|
|
8
|
+
<div class="cart-empty">
|
|
9
|
+
<h1 class="cart-empty-title">Loading your cart...</h1>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</BaseLayout>
|
|
13
|
+
|
|
14
|
+
<script>
|
|
15
|
+
import '../scripts/cart-page.js'
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../../styles/pages/plp.css'
|
|
3
|
+
import '../../styles/pages/home.css'
|
|
4
|
+
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
5
|
+
import Breadcrumb from '../../components/Breadcrumb.astro'
|
|
6
|
+
import ProductCard from '../../components/ProductCard.astro'
|
|
7
|
+
import SortDropdown from '../../components/SortDropdown.astro'
|
|
8
|
+
import Pagination from '../../components/Pagination.astro'
|
|
9
|
+
import { fetchProducts, fetchCategories, sortFromUrl } from '../../lib/api'
|
|
10
|
+
|
|
11
|
+
const { slug } = Astro.params
|
|
12
|
+
const sort = sortFromUrl(Astro.url)
|
|
13
|
+
|
|
14
|
+
const [productResult, categories] = await Promise.all([
|
|
15
|
+
fetchProducts({ limit: 24, category: slug, sort }),
|
|
16
|
+
fetchCategories(),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
const products = productResult.items
|
|
20
|
+
const category = categories.find((c) => c.slug === slug)
|
|
21
|
+
const categoryName = category?.name ?? slug ?? 'Category'
|
|
22
|
+
const facets = productResult.facets ?? []
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<BaseLayout
|
|
26
|
+
title={`${categoryName} | Nymbal`}
|
|
27
|
+
description={category?.description ?? `Browse ${categoryName} products.`}
|
|
28
|
+
>
|
|
29
|
+
<div class="container section">
|
|
30
|
+
<Breadcrumb items={[{ label: 'Home', href: '/' }, { label: categoryName }]} />
|
|
31
|
+
|
|
32
|
+
<div class="plp-header">
|
|
33
|
+
<h1 class="plp-title">{categoryName}</h1>
|
|
34
|
+
{category?.description && (
|
|
35
|
+
<p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a); margin-bottom: var(--nymbal-spacing-sm, 0.5rem);">
|
|
36
|
+
{category.description}
|
|
37
|
+
</p>
|
|
38
|
+
)}
|
|
39
|
+
<p class="plp-count" id="product-count">{products.length} products</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="plp-layout">
|
|
43
|
+
<aside class="plp-sidebar">
|
|
44
|
+
<nymbal-product-filter
|
|
45
|
+
facets={JSON.stringify(facets)}
|
|
46
|
+
></nymbal-product-filter>
|
|
47
|
+
</aside>
|
|
48
|
+
|
|
49
|
+
<div class="plp-grid-area">
|
|
50
|
+
<SortDropdown sort={sort} />
|
|
51
|
+
|
|
52
|
+
<div class="product-grid" id="product-grid" data-category={slug}>
|
|
53
|
+
{products.map((product) => (
|
|
54
|
+
<ProductCard product={product} />
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{products.length === 0 && (
|
|
59
|
+
<div class="plp-empty">
|
|
60
|
+
<h2 class="plp-empty-title">No products found</h2>
|
|
61
|
+
<p class="plp-empty-text">Check back soon for new arrivals.</p>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<Pagination />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</BaseLayout>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import '../../scripts/plp.js'
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../styles/pages/checkout.css'
|
|
3
|
+
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
|
+
import CheckoutSteps from '../components/CheckoutSteps.astro'
|
|
5
|
+
import AddressForm from '../components/AddressForm.astro'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<BaseLayout title="Checkout | Nymbal" description="Complete your purchase.">
|
|
9
|
+
<div class="container section">
|
|
10
|
+
<h1 style="font-size: 1.75rem; font-weight: var(--nymbal-font-weight-bold, 700); margin-bottom: var(--nymbal-spacing-md, 1rem);">Checkout</h1>
|
|
11
|
+
|
|
12
|
+
<div id="checkout-steps-container">
|
|
13
|
+
<CheckoutSteps currentStep="contact" />
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="checkout-error" id="checkout-error" style="display:none;" data-testid="checkout-error" role="alert"></div>
|
|
17
|
+
|
|
18
|
+
<div class="checkout-layout">
|
|
19
|
+
<div class="checkout-form">
|
|
20
|
+
<!-- Contact Section -->
|
|
21
|
+
<section class="checkout-section" id="checkout-contact" data-testid="checkout-contact">
|
|
22
|
+
<h2 class="checkout-section-title">Contact Information</h2>
|
|
23
|
+
<form id="contact-form">
|
|
24
|
+
<div class="form-group">
|
|
25
|
+
<label for="contact-email">Email Address</label>
|
|
26
|
+
<input
|
|
27
|
+
type="email"
|
|
28
|
+
id="contact-email"
|
|
29
|
+
name="email"
|
|
30
|
+
required
|
|
31
|
+
autocomplete="email"
|
|
32
|
+
placeholder="you@example.com"
|
|
33
|
+
data-testid="contact-email"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
<button type="submit" class="btn btn-primary" data-testid="contact-continue">
|
|
37
|
+
Continue to Shipping
|
|
38
|
+
</button>
|
|
39
|
+
</form>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<!-- Shipping Section -->
|
|
43
|
+
<section class="checkout-section" id="checkout-shipping" style="display:none;" data-testid="checkout-shipping">
|
|
44
|
+
<h2 class="checkout-section-title">Shipping Address</h2>
|
|
45
|
+
<form id="shipping-form">
|
|
46
|
+
<AddressForm id="shipping" />
|
|
47
|
+
<button type="submit" class="btn btn-primary" style="margin-top: var(--nymbal-spacing-md, 1rem);" data-testid="shipping-continue">
|
|
48
|
+
Continue to Payment
|
|
49
|
+
</button>
|
|
50
|
+
</form>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<!-- Payment Section -->
|
|
54
|
+
<section class="checkout-section" id="checkout-payment" style="display:none;" data-testid="checkout-payment">
|
|
55
|
+
<h2 class="checkout-section-title">Payment</h2>
|
|
56
|
+
<form id="payment-form" novalidate>
|
|
57
|
+
<div class="form-group">
|
|
58
|
+
<label>
|
|
59
|
+
<input type="checkbox" id="billing-same" checked />
|
|
60
|
+
Billing address same as shipping
|
|
61
|
+
</label>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div id="billing-fields" style="display:none;">
|
|
65
|
+
<h3 class="checkout-section-title" style="font-size: 1rem; margin-top: var(--nymbal-spacing-md, 1rem);">Billing Address</h3>
|
|
66
|
+
<div id="billing-form">
|
|
67
|
+
<AddressForm id="billing" />
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="payment-section" style="margin-top: var(--nymbal-spacing-md, 1rem);">
|
|
72
|
+
<label style="margin-bottom: var(--nymbal-spacing-sm, 0.5rem);">Card Details</label>
|
|
73
|
+
<div id="stripe-card-element" style="padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem); border: 1px solid var(--nymbal-color-border, #e4e4e7); border-radius: var(--nymbal-radius-md, 0.5rem); background: var(--nymbal-color-surface, #fff); min-height: 40px;" data-testid="stripe-card-element">
|
|
74
|
+
<!-- Stripe CardElement mounts here -->
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="checkout-submit">
|
|
79
|
+
<button type="submit" class="btn btn-primary" data-testid="place-order">
|
|
80
|
+
Place Order
|
|
81
|
+
</button>
|
|
82
|
+
<p class="checkout-terms">
|
|
83
|
+
By placing your order, you agree to our terms and conditions.
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
</form>
|
|
87
|
+
</section>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<aside class="checkout-summary" id="checkout-summary" data-testid="checkout-summary">
|
|
91
|
+
<h2 class="checkout-summary-title">Order Summary</h2>
|
|
92
|
+
<p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a);">Loading cart...</p>
|
|
93
|
+
</aside>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</BaseLayout>
|
|
97
|
+
|
|
98
|
+
<script>
|
|
99
|
+
import '../scripts/checkout.js'
|
|
100
|
+
</script>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CategoryNav from '../../components/CategoryNav.astro'
|
|
3
|
+
import { fetchCategories } from '../../lib/api'
|
|
4
|
+
|
|
5
|
+
Astro.response.headers.set('Cache-Control', 'public, s-maxage=21600, stale-while-revalidate=300')
|
|
6
|
+
|
|
7
|
+
const categories = await fetchCategories()
|
|
8
|
+
---
|
|
9
|
+
<CategoryNav categories={categories} />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import ProductCard from '../../../components/ProductCard.astro'
|
|
3
|
+
import { fetchProduct } from '../../../lib/api'
|
|
4
|
+
|
|
5
|
+
Astro.response.headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=60')
|
|
6
|
+
|
|
7
|
+
const { slug } = Astro.params
|
|
8
|
+
const product = await fetchProduct(slug!)
|
|
9
|
+
|
|
10
|
+
if (!product) {
|
|
11
|
+
return new Response('Not found', { status: 404 })
|
|
12
|
+
}
|
|
13
|
+
---
|
|
14
|
+
<ProductCard product={product} />
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../styles/pages/home.css'
|
|
3
|
+
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
|
+
import HeroSection from '../components/HeroSection.astro'
|
|
5
|
+
import CategoryNav from '../components/CategoryNav.astro'
|
|
6
|
+
import ProductCard from '../components/ProductCard.astro'
|
|
7
|
+
import NewsletterSignup from '../components/NewsletterSignup.astro'
|
|
8
|
+
import { fetchProducts, fetchCategories } from '../lib/api'
|
|
9
|
+
import { organizationJsonLd } from '../lib/seo'
|
|
10
|
+
|
|
11
|
+
const [productResult, categories] = await Promise.all([
|
|
12
|
+
fetchProducts({ limit: 8 }),
|
|
13
|
+
fetchCategories(),
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
const products = productResult.items
|
|
17
|
+
const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<BaseLayout title="Nymbal — Modern Storefront" jsonLd={jsonLd}>
|
|
21
|
+
<HeroSection />
|
|
22
|
+
|
|
23
|
+
<section class="section container">
|
|
24
|
+
<h2 class="section-title">Shop by Category</h2>
|
|
25
|
+
<CategoryNav categories={categories} />
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<section class="featured-section section">
|
|
29
|
+
<div class="container">
|
|
30
|
+
<div class="featured-header">
|
|
31
|
+
<h2 class="section-title" style="margin-bottom: 0;">Featured Products</h2>
|
|
32
|
+
<a href="/products" class="featured-link">View all</a>
|
|
33
|
+
</div>
|
|
34
|
+
{products.length > 0 ? (
|
|
35
|
+
<div class="product-grid">
|
|
36
|
+
{products.map((product) => (
|
|
37
|
+
<ProductCard product={product} />
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
) : (
|
|
41
|
+
<p style="text-align: center; color: var(--nymbal-color-text-muted, #71717a);">
|
|
42
|
+
Products coming soon.
|
|
43
|
+
</p>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<NewsletterSignup />
|
|
49
|
+
</BaseLayout>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../../styles/pages/confirmation.css'
|
|
3
|
+
import BaseLayout from '../../layouts/BaseLayout.astro'
|
|
4
|
+
import OrderSummary from '../../components/OrderSummary.astro'
|
|
5
|
+
import { fetchOrder } from '../../lib/api'
|
|
6
|
+
import { formatPrice } from '../../lib/format'
|
|
7
|
+
import { isPaid, orderHeading } from '../../lib/order-status'
|
|
8
|
+
|
|
9
|
+
const { orderNumber } = Astro.params
|
|
10
|
+
const order = await fetchOrder(orderNumber!)
|
|
11
|
+
|
|
12
|
+
if (!order) return Astro.redirect('/404')
|
|
13
|
+
|
|
14
|
+
const lineItems = (order.lineItems ?? []).map((item) => ({
|
|
15
|
+
name: `${item.productName}${item.variantName ? ` - ${item.variantName}` : ''}`,
|
|
16
|
+
qty: item.qty,
|
|
17
|
+
priceMinor: item.unitPriceMinor,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const shippingAddress = order.shippingAddress
|
|
21
|
+
const heading = orderHeading(order.status)
|
|
22
|
+
const paid = isPaid(order.status)
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<BaseLayout title={`${heading} | Nymbal`} description="Your order with Nymbal.">
|
|
26
|
+
<div class="container section">
|
|
27
|
+
<div class="confirmation-page" data-order-number={order.orderNumber} data-order-status={order.status}>
|
|
28
|
+
{paid && (
|
|
29
|
+
<div class="confirmation-icon">
|
|
30
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
31
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
32
|
+
</svg>
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
<h1 class="confirmation-title" data-testid="order-heading">{heading}</h1>
|
|
37
|
+
{paid ? (
|
|
38
|
+
<p class="confirmation-subtitle">
|
|
39
|
+
Thank you. Your order <strong data-testid="order-number">{order.orderNumber}</strong> is paid.
|
|
40
|
+
We'll send a confirmation to <strong>{order.email}</strong>.
|
|
41
|
+
</p>
|
|
42
|
+
) : order.status === 'pending' ? (
|
|
43
|
+
<p class="confirmation-subtitle">
|
|
44
|
+
Your order <strong data-testid="order-number">{order.orderNumber}</strong> is waiting for its payment.
|
|
45
|
+
We'll email <strong>{order.email}</strong> once it arrives.
|
|
46
|
+
</p>
|
|
47
|
+
) : (
|
|
48
|
+
<p class="confirmation-subtitle">
|
|
49
|
+
Your order <strong data-testid="order-number">{order.orderNumber}</strong> is {order.status.replace(/_/g, ' ')}.
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
<div class="confirmation-card">
|
|
54
|
+
<h2 class="confirmation-card-title">Order Details</h2>
|
|
55
|
+
|
|
56
|
+
<dl>
|
|
57
|
+
<div class="confirmation-detail-row">
|
|
58
|
+
<dt>Order Number</dt>
|
|
59
|
+
<dd>{order.orderNumber}</dd>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="confirmation-detail-row">
|
|
62
|
+
<dt>Status</dt>
|
|
63
|
+
<dd style="text-transform: capitalize;" data-testid="order-status">{order.status}</dd>
|
|
64
|
+
</div>
|
|
65
|
+
</dl>
|
|
66
|
+
|
|
67
|
+
<div class="confirmation-items">
|
|
68
|
+
{lineItems.map((item) => (
|
|
69
|
+
<div class="confirmation-item">
|
|
70
|
+
<span>
|
|
71
|
+
<span class="confirmation-item-name">{item.name}</span>
|
|
72
|
+
<span class="confirmation-item-qty"> x{item.qty}</span>
|
|
73
|
+
</span>
|
|
74
|
+
<span class="confirmation-item-price">
|
|
75
|
+
{formatPrice(item.priceMinor * item.qty, order.currency)}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="confirmation-totals">
|
|
82
|
+
<div class="confirmation-total-row">
|
|
83
|
+
<span>Subtotal</span>
|
|
84
|
+
<span>{formatPrice(order.subtotalMinor, order.currency)}</span>
|
|
85
|
+
</div>
|
|
86
|
+
{order.shippingTotalMinor != null && (
|
|
87
|
+
<div class="confirmation-total-row">
|
|
88
|
+
<span>Shipping</span>
|
|
89
|
+
<span>{order.shippingTotalMinor === 0 ? 'Free' : formatPrice(order.shippingTotalMinor, order.currency)}</span>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
{order.taxTotalMinor != null && (
|
|
93
|
+
<div class="confirmation-total-row">
|
|
94
|
+
<span>Tax</span>
|
|
95
|
+
<span>{formatPrice(order.taxTotalMinor, order.currency)}</span>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
<div class="confirmation-total-row grand-total">
|
|
99
|
+
<span>Total</span>
|
|
100
|
+
<span>{formatPrice(order.totalMinor, order.currency)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{shippingAddress && (
|
|
106
|
+
<div class="confirmation-card">
|
|
107
|
+
<h2 class="confirmation-card-title">Shipping Address</h2>
|
|
108
|
+
<p style="font-size: var(--nymbal-font-size-sm, 0.875rem); color: var(--nymbal-color-text-muted, #71717a); line-height: 1.6;">
|
|
109
|
+
{shippingAddress.addressLine1}<br />
|
|
110
|
+
{shippingAddress.addressLine2 && <>{shippingAddress.addressLine2}<br /></>}
|
|
111
|
+
{shippingAddress.city}, {shippingAddress.region} {shippingAddress.postalCode}<br />
|
|
112
|
+
{shippingAddress.country}
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<div class="confirmation-actions">
|
|
118
|
+
<a href="/account/register" class="btn btn-primary">Create an Account</a>
|
|
119
|
+
<a href="/products" class="btn btn-secondary">Continue Shopping</a>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</BaseLayout>
|
|
124
|
+
|
|
125
|
+
<script>
|
|
126
|
+
import '../../scripts/order-status.js'
|
|
127
|
+
</script>
|