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,116 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, type FormEvent } from 'react'
4
+ import styles from '@/styles/pages/pdp.module.css'
5
+ import { getNymbalClient } from '@/lib/client'
6
+
7
+ interface ReviewFormProps {
8
+ productId: string
9
+ }
10
+
11
+ export function ReviewForm({ productId }: ReviewFormProps) {
12
+ const [rating, setRating] = useState(5)
13
+ const [title, setTitle] = useState('')
14
+ const [body, setBody] = useState('')
15
+ const [submitting, setSubmitting] = useState(false)
16
+ const [success, setSuccess] = useState(false)
17
+ const [error, setError] = useState<string | null>(null)
18
+
19
+ const handleSubmit = useCallback(
20
+ async (e: FormEvent) => {
21
+ e.preventDefault()
22
+ setSubmitting(true)
23
+ setError(null)
24
+
25
+ try {
26
+ await getNymbalClient().adapter.reviews.submit({ productId, rating, title, body })
27
+ setSuccess(true)
28
+ setTitle('')
29
+ setBody('')
30
+ setRating(5)
31
+ } catch {
32
+ setError('Failed to submit review.')
33
+ } finally {
34
+ setSubmitting(false)
35
+ }
36
+ },
37
+ [productId, rating, title, body],
38
+ )
39
+
40
+ if (success) {
41
+ return (
42
+ <div className={styles.reviewsSection}>
43
+ <h3 className={styles.reviewsTitle}>Write a Review</h3>
44
+ <p style={{ fontSize: 'var(--nymbal-font-size-sm)', color: 'var(--nymbal-color-success)' }}>
45
+ Thank you for your review! It has been submitted successfully.
46
+ </p>
47
+ </div>
48
+ )
49
+ }
50
+
51
+ return (
52
+ <div className={styles.reviewsSection}>
53
+ <h3 className={styles.reviewsTitle}>Write a Review</h3>
54
+
55
+ {error && (
56
+ <p
57
+ style={{
58
+ fontSize: 'var(--nymbal-font-size-sm)',
59
+ color: 'var(--nymbal-color-error)',
60
+ marginBottom: 'var(--nymbal-spacing-md)',
61
+ }}
62
+ >
63
+ {error}
64
+ </p>
65
+ )}
66
+
67
+ <form onSubmit={handleSubmit} data-testid="review-form">
68
+ <div className="form-group">
69
+ <label htmlFor="review-rating">Rating</label>
70
+ <div className={styles.reviewStars} style={{ cursor: 'pointer', fontSize: '1.5rem' }}>
71
+ {Array.from({ length: 5 }, (_, i) => (
72
+ <span
73
+ key={i}
74
+ onClick={() => setRating(i + 1)}
75
+ className={i < rating ? styles.starFilled : styles.starEmpty}
76
+ role="button"
77
+ tabIndex={0}
78
+ aria-label={`Rate ${i + 1} star${i === 0 ? '' : 's'}`}
79
+ onKeyDown={(e) => {
80
+ if (e.key === 'Enter' || e.key === ' ') setRating(i + 1)
81
+ }}
82
+ >
83
+ ★
84
+ </span>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ <div className="form-group">
89
+ <label htmlFor="review-title">Title</label>
90
+ <input
91
+ id="review-title"
92
+ type="text"
93
+ value={title}
94
+ onChange={(e) => setTitle(e.target.value)}
95
+ required
96
+ placeholder="Summarise your experience"
97
+ />
98
+ </div>
99
+ <div className="form-group">
100
+ <label htmlFor="review-body">Review</label>
101
+ <textarea
102
+ id="review-body"
103
+ value={body}
104
+ onChange={(e) => setBody(e.target.value)}
105
+ required
106
+ rows={4}
107
+ placeholder="Tell us what you thought..."
108
+ />
109
+ </div>
110
+ <button type="submit" className="btn btn-primary" disabled={submitting}>
111
+ {submitting ? 'Submitting...' : 'Submit Review'}
112
+ </button>
113
+ </form>
114
+ </div>
115
+ )
116
+ }
@@ -0,0 +1,27 @@
1
+ 'use client'
2
+
3
+ import { useCallback } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { SearchBar } from '@nymbalhq/react'
6
+
7
+ interface SearchProduct {
8
+ slug: string
9
+ }
10
+
11
+ export function SearchBarWrapper() {
12
+ const router = useRouter()
13
+
14
+ const handleSelect = useCallback(
15
+ (event: CustomEvent<{ product: unknown }>) => {
16
+ const product = event.detail.product as SearchProduct
17
+ if (product?.slug) {
18
+ router.push(`/product/${encodeURIComponent(product.slug)}`)
19
+ }
20
+ },
21
+ [router],
22
+ )
23
+
24
+ return (
25
+ <SearchBar placeholder="Search products..." debounceMs={300} onSearchSelect={handleSelect} />
26
+ )
27
+ }
@@ -0,0 +1,141 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import Image from 'next/image'
7
+ import { useSearch } from '@nymbalhq/react'
8
+ import { formatPrice } from '@/lib/format'
9
+ import styles from '@/styles/pages/search.module.css'
10
+
11
+ export function SearchResults() {
12
+ const searchParams = useSearchParams()
13
+ const query = searchParams.get('q') ?? ''
14
+ const { results, loading, search } = useSearch()
15
+
16
+ useEffect(() => {
17
+ if (query) {
18
+ search(query)
19
+ }
20
+ }, [query, search])
21
+
22
+ return (
23
+ <div className={`container ${styles.page}`}>
24
+ <div className={styles.header}>
25
+ <h1 className={styles.pageTitle}>Search Results</h1>
26
+ {query && (
27
+ <p className={styles.queryText}>
28
+ Showing results for <span className={styles.queryTerm}>&ldquo;{query}&rdquo;</span>
29
+ </p>
30
+ )}
31
+ </div>
32
+
33
+ {loading ? (
34
+ <div className={styles.loading}>
35
+ <div className="spinner spinner-lg" role="status">
36
+ <span className="visually-hidden">Searching...</span>
37
+ </div>
38
+ </div>
39
+ ) : !query ? (
40
+ <div className={styles.empty}>
41
+ <div className={styles.emptyIcon}>
42
+ <svg
43
+ viewBox="0 0 24 24"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ strokeWidth="2"
47
+ strokeLinecap="round"
48
+ strokeLinejoin="round"
49
+ aria-hidden="true"
50
+ >
51
+ <circle cx="11" cy="11" r="8" />
52
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
53
+ </svg>
54
+ </div>
55
+ <h2 className={styles.emptyTitle}>Search our store</h2>
56
+ <p className={styles.emptyText}>
57
+ Use the search bar above to find what you are looking for.
58
+ </p>
59
+ </div>
60
+ ) : results && results.length > 0 ? (
61
+ <>
62
+ <p className={styles.resultCount}>
63
+ {results.length} result{results.length !== 1 ? 's' : ''} found
64
+ </p>
65
+ <div className={styles.grid}>
66
+ {results.map((product) => {
67
+ const media = Array.isArray(product.media) ? product.media : []
68
+ const imageUrl = media.length > 0 && typeof (media[0] as Record<string, unknown>)?.url === 'string'
69
+ ? (media[0] as Record<string, string>).url
70
+ : null
71
+ return (
72
+ <Link
73
+ key={product.id}
74
+ href={`/product/${product.slug}`}
75
+ className={styles.productCard}
76
+ data-testid={`product-card-${product.slug}`}
77
+ >
78
+ <div className={styles.productImage}>
79
+ {imageUrl && (
80
+ <Image
81
+ src={imageUrl}
82
+ alt={product.name}
83
+ width={400}
84
+ height={400}
85
+ sizes="(max-width: 639px) 100vw, (max-width: 1023px) 50vw, 25vw"
86
+ />
87
+ )}
88
+ </div>
89
+ <div className={styles.productInfo}>
90
+ <p className={styles.productName}>{product.name}</p>
91
+ {product.priceMinor != null && product.currency && (
92
+ <p className={styles.productPrice}>
93
+ {formatPrice(product.priceMinor, product.currency)}
94
+ </p>
95
+ )}
96
+ </div>
97
+ </Link>
98
+ )
99
+ })}
100
+ </div>
101
+ </>
102
+ ) : (
103
+ <div className={styles.empty}>
104
+ <div className={styles.emptyIcon}>
105
+ <svg
106
+ viewBox="0 0 24 24"
107
+ fill="none"
108
+ stroke="currentColor"
109
+ strokeWidth="2"
110
+ strokeLinecap="round"
111
+ strokeLinejoin="round"
112
+ aria-hidden="true"
113
+ >
114
+ <circle cx="11" cy="11" r="8" />
115
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
116
+ </svg>
117
+ </div>
118
+ <h2 className={styles.emptyTitle}>No results found</h2>
119
+ <p className={styles.emptyText}>
120
+ We could not find anything matching &ldquo;{query}&rdquo;. Try a different search term.
121
+ </p>
122
+ </div>
123
+ )}
124
+
125
+ <div className={styles.suggestions}>
126
+ <h3 className={styles.suggestionsTitle}>Popular searches</h3>
127
+ <div className={styles.suggestionTags}>
128
+ {['Shirts', 'Shoes', 'Jackets', 'Accessories', 'New Arrivals'].map((term) => (
129
+ <Link
130
+ key={term}
131
+ href={`/search?q=${encodeURIComponent(term)}`}
132
+ className={styles.suggestionTag}
133
+ >
134
+ {term}
135
+ </Link>
136
+ ))}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ )
141
+ }
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import type { ProductSortKey } from '@nymbalhq/types'
4
+ import styles from '@/styles/pages/plp.module.css'
5
+
6
+ const OPTIONS: Array<{ value: ProductSortKey; label: string }> = [
7
+ { value: 'newest', label: 'Newest' },
8
+ { value: 'price-asc', label: 'Price: Low to High' },
9
+ { value: 'price-desc', label: 'Price: High to Low' },
10
+ { value: 'name-asc', label: 'Name: A to Z' },
11
+ { value: 'name-desc', label: 'Name: Z to A' },
12
+ ]
13
+
14
+ interface SortDropdownProps {
15
+ value: ProductSortKey
16
+ onChange: (sort: ProductSortKey) => void
17
+ }
18
+
19
+ export function SortDropdown({ value, onChange }: SortDropdownProps) {
20
+ return (
21
+ <select
22
+ className={styles.sortSelect}
23
+ value={value}
24
+ onChange={(e) => onChange(e.target.value as ProductSortKey)}
25
+ data-testid="sort-dropdown"
26
+ aria-label="Sort products"
27
+ >
28
+ {OPTIONS.map((option) => (
29
+ <option key={option.value} value={option.value}>
30
+ {option.label}
31
+ </option>
32
+ ))}
33
+ </select>
34
+ )
35
+ }
@@ -0,0 +1,36 @@
1
+ import Link from 'next/link'
2
+ import styles from '@/styles/pages/pdp.module.css'
3
+
4
+ interface BreadcrumbItem {
5
+ label: string
6
+ href?: string
7
+ }
8
+
9
+ interface BreadcrumbProps {
10
+ items: BreadcrumbItem[]
11
+ }
12
+
13
+ export function Breadcrumb({ items }: BreadcrumbProps) {
14
+ return (
15
+ <nav aria-label="Breadcrumb" data-testid="breadcrumb">
16
+ <ol className={styles.breadcrumb}>
17
+ {items.map((item, index) => (
18
+ <li key={index} style={{ display: 'inline-flex', alignItems: 'center', gap: 'inherit' }}>
19
+ {index > 0 && (
20
+ <span className={styles.breadcrumbSeparator} aria-hidden="true">
21
+ /
22
+ </span>
23
+ )}
24
+ {item.href ? (
25
+ <Link href={item.href} className={styles.breadcrumbLink}>
26
+ {item.label}
27
+ </Link>
28
+ ) : (
29
+ <span aria-current="page">{item.label}</span>
30
+ )}
31
+ </li>
32
+ ))}
33
+ </ol>
34
+ </nav>
35
+ )
36
+ }
@@ -0,0 +1,29 @@
1
+ import Link from 'next/link'
2
+ import type { Category } from '@nymbalhq/types'
3
+ import styles from '@/styles/pages/home.module.css'
4
+
5
+ interface CategoryNavProps {
6
+ categories: Category[]
7
+ }
8
+
9
+ export function CategoryNav({ categories }: CategoryNavProps) {
10
+ if (categories.length === 0) return null
11
+
12
+ return (
13
+ <section className={`container ${styles.categoriesSection}`}>
14
+ <h2 className="section-title">Shop by Category</h2>
15
+ <div className={styles.categoriesGrid}>
16
+ {categories.map((category) => (
17
+ <Link
18
+ key={category.id}
19
+ href={`/category/${category.slug}`}
20
+ className={styles.categoryCard}
21
+ data-testid={`category-${category.slug}`}
22
+ >
23
+ {category.name}
24
+ </Link>
25
+ ))}
26
+ </div>
27
+ </section>
28
+ )
29
+ }
@@ -0,0 +1,60 @@
1
+ import styles from '@/styles/pages/checkout.module.css'
2
+
3
+ const STEPS = [
4
+ { label: 'Contact' },
5
+ { label: 'Shipping' },
6
+ { label: 'Payment' },
7
+ ]
8
+
9
+ interface CheckoutStepsProps {
10
+ currentStep: number
11
+ }
12
+
13
+ export function CheckoutSteps({ currentStep }: CheckoutStepsProps) {
14
+ return (
15
+ <div className={styles.steps} data-testid="checkout-steps">
16
+ {STEPS.map((step, index) => {
17
+ const isCompleted = index < currentStep
18
+ const isActive = index === currentStep
19
+
20
+ return (
21
+ <div key={step.label} style={{ display: 'contents' }}>
22
+ {index > 0 && (
23
+ <div
24
+ className={`${styles.stepConnector} ${
25
+ isCompleted ? styles.stepConnectorCompleted : ''
26
+ }`}
27
+ />
28
+ )}
29
+ <div
30
+ className={`${styles.step} ${isActive ? styles.stepActive : ''} ${
31
+ isCompleted ? styles.stepCompleted : ''
32
+ }`}
33
+ >
34
+ <span className={styles.stepNumber}>
35
+ {isCompleted ? (
36
+ <svg
37
+ width="14"
38
+ height="14"
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ strokeWidth="3"
43
+ strokeLinecap="round"
44
+ strokeLinejoin="round"
45
+ aria-hidden="true"
46
+ >
47
+ <polyline points="20 6 9 17 4 12" />
48
+ </svg>
49
+ ) : (
50
+ index + 1
51
+ )}
52
+ </span>
53
+ <span className={styles.stepLabel}>{step.label}</span>
54
+ </div>
55
+ </div>
56
+ )
57
+ })}
58
+ </div>
59
+ )
60
+ }
@@ -0,0 +1,64 @@
1
+ .footer {
2
+ background-color: #fafafa;
3
+ border-top: 1px solid var(--nymbal-color-border);
4
+ margin-top: var(--nymbal-spacing-xl);
5
+ }
6
+
7
+ .inner {
8
+ padding-block: var(--nymbal-spacing-xl);
9
+ }
10
+
11
+ .columns {
12
+ display: grid;
13
+ grid-template-columns: 1fr;
14
+ gap: var(--nymbal-spacing-xl);
15
+ }
16
+
17
+ @media (min-width: 640px) {
18
+ .columns {
19
+ grid-template-columns: repeat(3, 1fr);
20
+ }
21
+ }
22
+
23
+ .column {
24
+ min-width: 0;
25
+ }
26
+
27
+ .heading {
28
+ font-size: var(--nymbal-font-size-sm);
29
+ font-weight: var(--nymbal-font-weight-bold);
30
+ text-transform: uppercase;
31
+ letter-spacing: 0.05em;
32
+ color: var(--nymbal-color-text);
33
+ margin-bottom: var(--nymbal-spacing-md);
34
+ }
35
+
36
+ .list {
37
+ list-style: none;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: var(--nymbal-spacing-sm);
41
+ }
42
+
43
+ .link {
44
+ font-size: var(--nymbal-font-size-sm);
45
+ color: var(--nymbal-color-text-muted);
46
+ text-decoration: none;
47
+ transition: color var(--nymbal-transition-fast);
48
+ }
49
+
50
+ .link:hover {
51
+ color: var(--nymbal-color-text);
52
+ text-decoration: none;
53
+ }
54
+
55
+ .bottom {
56
+ margin-top: var(--nymbal-spacing-xl);
57
+ padding-top: var(--nymbal-spacing-lg);
58
+ border-top: 1px solid var(--nymbal-color-border);
59
+ }
60
+
61
+ .copyright {
62
+ font-size: var(--nymbal-font-size-xs);
63
+ color: var(--nymbal-color-text-muted);
64
+ }
@@ -0,0 +1,79 @@
1
+ import Link from 'next/link'
2
+ import styles from './Footer.module.css'
3
+
4
+ const SHOP_LINKS = [
5
+ { href: '/products', label: 'All Products' },
6
+ { href: '/products?category=new', label: 'New Arrivals' },
7
+ { href: '/products?category=sale', label: 'Sale' },
8
+ ]
9
+
10
+ const ACCOUNT_LINKS = [
11
+ { href: '/account', label: 'My Account' },
12
+ { href: '/account/orders', label: 'Order History' },
13
+ { href: '/account/login', label: 'Sign In' },
14
+ { href: '/account/register', label: 'Create Account' },
15
+ ]
16
+
17
+ const ABOUT_LINKS = [
18
+ { href: '/about', label: 'About Us' },
19
+ { href: '/contact', label: 'Contact' },
20
+ { href: '/shipping', label: 'Shipping & Returns' },
21
+ { href: '/privacy', label: 'Privacy Policy' },
22
+ ]
23
+
24
+ export function Footer() {
25
+ const year = new Date().getFullYear()
26
+
27
+ return (
28
+ <footer className={styles.footer} data-testid="site-footer">
29
+ <div className={`container ${styles.inner}`}>
30
+ <div className={styles.columns}>
31
+ <div className={styles.column}>
32
+ <h2 className={styles.heading}>Shop</h2>
33
+ <ul className={styles.list}>
34
+ {SHOP_LINKS.map((link) => (
35
+ <li key={link.href}>
36
+ <Link href={link.href} className={styles.link}>
37
+ {link.label}
38
+ </Link>
39
+ </li>
40
+ ))}
41
+ </ul>
42
+ </div>
43
+
44
+ <div className={styles.column}>
45
+ <h2 className={styles.heading}>Account</h2>
46
+ <ul className={styles.list}>
47
+ {ACCOUNT_LINKS.map((link) => (
48
+ <li key={link.href}>
49
+ <Link href={link.href} className={styles.link}>
50
+ {link.label}
51
+ </Link>
52
+ </li>
53
+ ))}
54
+ </ul>
55
+ </div>
56
+
57
+ <div className={styles.column}>
58
+ <h2 className={styles.heading}>About</h2>
59
+ <ul className={styles.list}>
60
+ {ABOUT_LINKS.map((link) => (
61
+ <li key={link.href}>
62
+ <Link href={link.href} className={styles.link}>
63
+ {link.label}
64
+ </Link>
65
+ </li>
66
+ ))}
67
+ </ul>
68
+ </div>
69
+ </div>
70
+
71
+ <div className={styles.bottom}>
72
+ <p className={styles.copyright}>
73
+ &copy; {year} Nymbal. All rights reserved.
74
+ </p>
75
+ </div>
76
+ </div>
77
+ </footer>
78
+ )
79
+ }