@tanstack/create 0.64.0 → 0.66.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 (195) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/create-app.js +72 -16
  3. package/dist/file-helpers.js +14 -0
  4. package/dist/frameworks/react/add-ons/ai/info.json +1 -1
  5. package/dist/frameworks/react/add-ons/better-auth/info.json +1 -1
  6. package/dist/frameworks/react/add-ons/clerk/README.md +42 -1
  7. package/dist/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx +94 -11
  8. package/dist/frameworks/react/add-ons/clerk/info.json +1 -1
  9. package/dist/frameworks/react/add-ons/compiler/info.json +1 -1
  10. package/dist/frameworks/react/add-ons/convex/info.json +1 -1
  11. package/dist/frameworks/react/add-ons/drizzle/info.json +1 -1
  12. package/dist/frameworks/react/add-ons/mcp/info.json +1 -1
  13. package/dist/frameworks/react/add-ons/neon/info.json +1 -1
  14. package/dist/frameworks/react/add-ons/paraglide/info.json +1 -1
  15. package/dist/frameworks/react/add-ons/prisma/info.json +1 -1
  16. package/dist/frameworks/react/add-ons/shadcn/info.json +1 -1
  17. package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
  18. package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  19. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  20. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  21. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  22. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  23. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  24. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  25. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  26. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  27. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  28. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  29. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  30. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  31. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  32. package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  33. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  34. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  35. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  36. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  37. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  38. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  39. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  40. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  41. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  42. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  43. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  44. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  45. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  46. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  47. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  48. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  49. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  50. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  51. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  52. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  53. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  54. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  55. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  56. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  57. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  58. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  59. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  60. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  61. package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
  62. package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
  63. package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  64. package/dist/frameworks/react/add-ons/strapi/info.json +1 -1
  65. package/dist/frameworks/react/add-ons/t3env/info.json +1 -1
  66. package/dist/frameworks/react/add-ons/workos/info.json +1 -1
  67. package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
  68. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  69. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  70. package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  71. package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
  72. package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
  73. package/dist/frameworks/react/hosts/cloudflare/README.md +11 -0
  74. package/dist/frameworks/react/hosts/cloudflare/info.json +1 -1
  75. package/dist/frameworks/react/hosts/netlify/README.md +11 -0
  76. package/dist/frameworks/react/hosts/netlify/info.json +1 -1
  77. package/dist/frameworks/react/hosts/nitro/README.md +12 -0
  78. package/dist/frameworks/react/hosts/nitro/info.json +1 -1
  79. package/dist/frameworks/react/hosts/railway/README.md +10 -0
  80. package/dist/frameworks/react/hosts/railway/info.json +1 -1
  81. package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
  82. package/dist/frameworks/solid/add-ons/better-auth/info.json +1 -1
  83. package/dist/frameworks/solid/add-ons/convex/info.json +1 -1
  84. package/dist/frameworks/solid/add-ons/solid-ui/info.json +1 -1
  85. package/dist/frameworks/solid/add-ons/strapi/info.json +1 -1
  86. package/dist/frameworks/solid/add-ons/t3env/info.json +1 -1
  87. package/dist/frameworks/solid/hosts/cloudflare/README.md +11 -0
  88. package/dist/frameworks/solid/hosts/cloudflare/info.json +1 -1
  89. package/dist/frameworks/solid/hosts/netlify/README.md +11 -0
  90. package/dist/frameworks/solid/hosts/netlify/info.json +1 -1
  91. package/dist/frameworks/solid/hosts/nitro/README.md +12 -0
  92. package/dist/frameworks/solid/hosts/nitro/info.json +1 -1
  93. package/dist/frameworks/solid/hosts/railway/README.md +10 -0
  94. package/dist/frameworks/solid/hosts/railway/info.json +1 -1
  95. package/dist/index.js +1 -1
  96. package/dist/integrations/intent.js +1 -1
  97. package/dist/types/file-helpers.d.ts +1 -0
  98. package/dist/types/index.d.ts +1 -1
  99. package/package.json +1 -1
  100. package/src/create-app.ts +86 -22
  101. package/src/file-helpers.ts +20 -0
  102. package/src/frameworks/react/add-ons/ai/info.json +1 -1
  103. package/src/frameworks/react/add-ons/better-auth/info.json +1 -1
  104. package/src/frameworks/react/add-ons/clerk/README.md +42 -1
  105. package/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx +94 -11
  106. package/src/frameworks/react/add-ons/clerk/info.json +1 -1
  107. package/src/frameworks/react/add-ons/compiler/info.json +1 -1
  108. package/src/frameworks/react/add-ons/convex/info.json +1 -1
  109. package/src/frameworks/react/add-ons/drizzle/info.json +1 -1
  110. package/src/frameworks/react/add-ons/mcp/info.json +1 -1
  111. package/src/frameworks/react/add-ons/neon/info.json +1 -1
  112. package/src/frameworks/react/add-ons/paraglide/info.json +1 -1
  113. package/src/frameworks/react/add-ons/prisma/info.json +1 -1
  114. package/src/frameworks/react/add-ons/shadcn/info.json +1 -1
  115. package/src/frameworks/react/add-ons/shopify/README.md +86 -0
  116. package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  117. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  118. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  119. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  120. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  121. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  122. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  123. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  124. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  125. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  126. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  127. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  128. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  129. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  130. package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  131. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  132. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  133. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  134. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  135. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  136. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  137. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  138. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  139. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  140. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  141. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  142. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  143. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  144. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  145. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  146. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  147. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  148. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  149. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  150. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  151. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  152. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  153. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  154. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  155. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  156. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  157. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  158. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  159. package/src/frameworks/react/add-ons/shopify/info.json +104 -0
  160. package/src/frameworks/react/add-ons/shopify/package.json +6 -0
  161. package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  162. package/src/frameworks/react/add-ons/strapi/info.json +1 -1
  163. package/src/frameworks/react/add-ons/t3env/info.json +1 -1
  164. package/src/frameworks/react/add-ons/workos/info.json +1 -1
  165. package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
  166. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  167. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  168. package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  169. package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
  170. package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
  171. package/src/frameworks/react/hosts/cloudflare/README.md +11 -0
  172. package/src/frameworks/react/hosts/cloudflare/info.json +1 -1
  173. package/src/frameworks/react/hosts/netlify/README.md +11 -0
  174. package/src/frameworks/react/hosts/netlify/info.json +1 -1
  175. package/src/frameworks/react/hosts/nitro/README.md +12 -0
  176. package/src/frameworks/react/hosts/nitro/info.json +1 -1
  177. package/src/frameworks/react/hosts/railway/README.md +10 -0
  178. package/src/frameworks/react/hosts/railway/info.json +1 -1
  179. package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
  180. package/src/frameworks/solid/add-ons/better-auth/info.json +1 -1
  181. package/src/frameworks/solid/add-ons/convex/info.json +1 -1
  182. package/src/frameworks/solid/add-ons/solid-ui/info.json +1 -1
  183. package/src/frameworks/solid/add-ons/strapi/info.json +1 -1
  184. package/src/frameworks/solid/add-ons/t3env/info.json +1 -1
  185. package/src/frameworks/solid/hosts/cloudflare/README.md +11 -0
  186. package/src/frameworks/solid/hosts/cloudflare/info.json +1 -1
  187. package/src/frameworks/solid/hosts/netlify/README.md +11 -0
  188. package/src/frameworks/solid/hosts/netlify/info.json +1 -1
  189. package/src/frameworks/solid/hosts/nitro/README.md +12 -0
  190. package/src/frameworks/solid/hosts/nitro/info.json +1 -1
  191. package/src/frameworks/solid/hosts/railway/README.md +10 -0
  192. package/src/frameworks/solid/hosts/railway/info.json +1 -1
  193. package/src/index.ts +1 -0
  194. package/src/integrations/intent.ts +1 -1
  195. package/tests/create-app.test.ts +95 -0
@@ -0,0 +1,29 @@
1
+ import { Link } from '@tanstack/react-router'
2
+ import type { ReactNode } from 'react'
3
+
4
+ type EmptyStateProps = {
5
+ title: string
6
+ description?: string
7
+ cta?: { label: string; to: string }
8
+ children?: ReactNode
9
+ }
10
+
11
+ export function EmptyState({ title, description, cta, children }: EmptyStateProps) {
12
+ return (
13
+ <div className="flex flex-col items-center gap-4 rounded-2xl border border-dashed border-[var(--storefront-line)] px-6 py-16 text-center">
14
+ <h2 className="text-xl font-medium">{title}</h2>
15
+ {description && (
16
+ <p className="max-w-md text-[var(--storefront-fg-muted)]">{description}</p>
17
+ )}
18
+ {children}
19
+ {cta && (
20
+ <Link
21
+ to={cta.to}
22
+ className="rounded-full border border-[var(--storefront-fg)] px-5 py-2 text-sm font-medium no-underline text-[var(--storefront-fg)]"
23
+ >
24
+ {cta.label}
25
+ </Link>
26
+ )}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,11 @@
1
+ import { formatMoney } from '#/lib/shopify/format'
2
+
3
+ type MoneyProps = {
4
+ amount: string | number
5
+ currencyCode: string
6
+ className?: string
7
+ }
8
+
9
+ export function Money({ amount, currencyCode, className }: MoneyProps) {
10
+ return <span className={className}>{formatMoney(amount, currencyCode)}</span>
11
+ }
@@ -0,0 +1,74 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ import { Money } from '#/components/shop/money'
4
+ import { ShopImage } from '#/components/shop/shop-image'
5
+ import type { ProductListItem } from '#/lib/shopify/queries'
6
+
7
+ type ProductCardProps = {
8
+ product: ProductListItem
9
+ }
10
+
11
+ export function ProductCard({ product }: ProductCardProps) {
12
+ const minPrice = product.priceRange.minVariantPrice
13
+ const maxPrice = product.priceRange.maxVariantPrice
14
+ const compareAt = product.compareAtPriceRange.minVariantPrice
15
+ const onSale =
16
+ Number(compareAt.amount) > 0 &&
17
+ Number(compareAt.amount) > Number(minPrice.amount)
18
+ const soldOut = !product.variants.nodes.some((v) => v.availableForSale)
19
+
20
+ return (
21
+ <Link
22
+ to="/shop/products/$handle"
23
+ params={{ handle: product.handle }}
24
+ className="group flex flex-col gap-3 no-underline text-[var(--storefront-fg)]"
25
+ >
26
+ <div
27
+ className="relative overflow-hidden bg-[var(--storefront-line)]"
28
+ style={{ aspectRatio: '4 / 5', borderRadius: 'var(--storefront-radius)' }}
29
+ >
30
+ <ShopImage
31
+ src={product.featuredImage?.url}
32
+ alt={product.featuredImage?.altText ?? product.title}
33
+ width={600}
34
+ height={750}
35
+ sizes="(min-width: 1024px) 25vw, (min-width: 640px) 33vw, 50vw"
36
+ className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
37
+ />
38
+ {soldOut && (
39
+ <span className="absolute left-3 top-3 rounded-full bg-[var(--storefront-bg)]/90 px-2.5 py-1 text-xs font-medium uppercase tracking-wide">
40
+ Sold out
41
+ </span>
42
+ )}
43
+ {!soldOut && onSale && (
44
+ <span className="absolute left-3 top-3 rounded-full bg-[var(--storefront-accent)] px-2.5 py-1 text-xs font-medium uppercase tracking-wide text-[var(--storefront-accent-fg)]">
45
+ Sale
46
+ </span>
47
+ )}
48
+ </div>
49
+ <div className="flex flex-col gap-1">
50
+ <h3 className="text-sm font-medium leading-snug">{product.title}</h3>
51
+ <p className="text-sm text-[var(--storefront-fg-muted)]">
52
+ {onSale && (
53
+ <span className="mr-2 line-through">
54
+ <Money
55
+ amount={compareAt.amount}
56
+ currencyCode={compareAt.currencyCode}
57
+ />
58
+ </span>
59
+ )}
60
+ <Money amount={minPrice.amount} currencyCode={minPrice.currencyCode} />
61
+ {Number(maxPrice.amount) > Number(minPrice.amount) && (
62
+ <>
63
+ {' – '}
64
+ <Money
65
+ amount={maxPrice.amount}
66
+ currencyCode={maxPrice.currencyCode}
67
+ />
68
+ </>
69
+ )}
70
+ </p>
71
+ </div>
72
+ </Link>
73
+ )
74
+ }
@@ -0,0 +1,24 @@
1
+ import { ProductCard } from '#/components/shop/product-card'
2
+ import type { ProductListItem } from '#/lib/shopify/queries'
3
+
4
+ type ProductGridProps = {
5
+ products: ReadonlyArray<ProductListItem>
6
+ }
7
+
8
+ export function ProductGrid({ products }: ProductGridProps) {
9
+ if (products.length === 0) {
10
+ return (
11
+ <p className="py-16 text-center text-[var(--storefront-fg-muted)]">
12
+ No products yet.
13
+ </p>
14
+ )
15
+ }
16
+
17
+ return (
18
+ <div className="grid grid-cols-2 gap-x-4 gap-y-10 sm:grid-cols-3 lg:grid-cols-4">
19
+ {products.map((product) => (
20
+ <ProductCard key={product.id} product={product} />
21
+ ))}
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,57 @@
1
+ import { shopifyImageUrl } from '#/lib/shopify/format'
2
+
3
+ type ShopImageProps = {
4
+ src: string | null | undefined
5
+ alt: string | null | undefined
6
+ width: number
7
+ height?: number
8
+ className?: string
9
+ sizes?: string
10
+ loading?: 'eager' | 'lazy'
11
+ }
12
+
13
+ export function ShopImage({
14
+ src,
15
+ alt,
16
+ width,
17
+ height,
18
+ className,
19
+ sizes,
20
+ loading = 'lazy',
21
+ }: ShopImageProps) {
22
+ if (!src) {
23
+ return (
24
+ <div
25
+ aria-hidden
26
+ className={className}
27
+ style={{
28
+ background: 'var(--storefront-line)',
29
+ aspectRatio: height ? `${width} / ${height}` : '1',
30
+ }}
31
+ />
32
+ )
33
+ }
34
+
35
+ const transformed = shopifyImageUrl(src, { width, height, format: 'webp' })
36
+ const srcSet = [1, 2]
37
+ .map((dpr) => {
38
+ const w = width * dpr
39
+ const h = height ? height * dpr : undefined
40
+ return `${shopifyImageUrl(src, { width: w, height: h, format: 'webp' })} ${dpr}x`
41
+ })
42
+ .join(', ')
43
+
44
+ return (
45
+ <img
46
+ src={transformed}
47
+ srcSet={srcSet}
48
+ sizes={sizes}
49
+ alt={alt ?? ''}
50
+ width={width}
51
+ height={height}
52
+ loading={loading}
53
+ decoding="async"
54
+ className={className}
55
+ />
56
+ )
57
+ }
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Shop theme tokens. Override these six variables in your own CSS to re-theme
3
+ * the storefront without touching components.
4
+ */
5
+ :root {
6
+ --storefront-bg: #ffffff;
7
+ --storefront-fg: #0a0a0a;
8
+ --storefront-fg-muted: #525252;
9
+ --storefront-line: #e5e5e5;
10
+ --storefront-accent: #0a0a0a;
11
+ --storefront-accent-fg: #ffffff;
12
+ --storefront-radius: 0.5rem;
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root:not([data-theme='light']) {
17
+ --storefront-bg: #0a0a0a;
18
+ --storefront-fg: #fafafa;
19
+ --storefront-fg-muted: #a3a3a3;
20
+ --storefront-line: #262626;
21
+ --storefront-accent: #fafafa;
22
+ --storefront-accent-fg: #0a0a0a;
23
+ }
24
+ }
25
+
26
+ :root[data-theme='dark'] {
27
+ --storefront-bg: #0a0a0a;
28
+ --storefront-fg: #fafafa;
29
+ --storefront-fg-muted: #a3a3a3;
30
+ --storefront-line: #262626;
31
+ --storefront-accent: #fafafa;
32
+ --storefront-accent-fg: #0a0a0a;
33
+ }
34
+
35
+ .shop-root {
36
+ background: var(--storefront-bg);
37
+ color: var(--storefront-fg);
38
+ }
39
+
40
+ .shop-prose {
41
+ max-width: 65ch;
42
+ line-height: 1.7;
43
+ }
44
+ .shop-prose h1,
45
+ .shop-prose h2,
46
+ .shop-prose h3 {
47
+ font-weight: 600;
48
+ letter-spacing: -0.02em;
49
+ margin-top: 1.5em;
50
+ }
51
+ .shop-prose p {
52
+ margin: 0.75em 0;
53
+ }
54
+ .shop-prose a {
55
+ color: var(--storefront-accent);
56
+ text-decoration: underline;
57
+ text-underline-offset: 2px;
58
+ }
@@ -0,0 +1,79 @@
1
+ import type { ProductDetail, ProductDetailVariant } from '#/lib/shopify/queries'
2
+
3
+ type VariantSelectorProps = {
4
+ product: ProductDetail
5
+ selectedOptions: Record<string, string>
6
+ onChange: (next: Record<string, string>) => void
7
+ }
8
+
9
+ export function VariantSelector({
10
+ product,
11
+ selectedOptions,
12
+ onChange,
13
+ }: VariantSelectorProps) {
14
+ if (product.options.length === 1 && product.options[0]?.values.length === 1) {
15
+ return null
16
+ }
17
+
18
+ return (
19
+ <div className="space-y-5">
20
+ {product.options.map((option) => (
21
+ <div key={option.id}>
22
+ <div className="mb-2 flex items-baseline justify-between">
23
+ <h3 className="text-sm font-medium">{option.name}</h3>
24
+ <span className="text-sm text-[var(--storefront-fg-muted)]">
25
+ {selectedOptions[option.name]}
26
+ </span>
27
+ </div>
28
+ <div className="flex flex-wrap gap-2">
29
+ {option.values.map((value) => {
30
+ const selected = selectedOptions[option.name] === value
31
+ const wouldBe = { ...selectedOptions, [option.name]: value }
32
+ const matchingVariant = findVariant(product.variants.nodes, wouldBe)
33
+ const available = matchingVariant?.availableForSale ?? false
34
+ return (
35
+ <button
36
+ key={value}
37
+ type="button"
38
+ onClick={() => onChange(wouldBe)}
39
+ disabled={!available && !selected}
40
+ className={[
41
+ 'min-w-[3rem] rounded-full border px-4 py-2 text-sm transition',
42
+ selected
43
+ ? 'border-[var(--storefront-accent)] bg-[var(--storefront-accent)] text-[var(--storefront-accent-fg)]'
44
+ : 'border-[var(--storefront-line)] hover:border-[var(--storefront-accent)]',
45
+ !available && !selected
46
+ ? 'cursor-not-allowed opacity-40 line-through'
47
+ : '',
48
+ ].join(' ')}
49
+ >
50
+ {value}
51
+ </button>
52
+ )
53
+ })}
54
+ </div>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ )
59
+ }
60
+
61
+ export function findVariant(
62
+ variants: ReadonlyArray<ProductDetailVariant>,
63
+ selected: Record<string, string>,
64
+ ): ProductDetailVariant | undefined {
65
+ return variants.find((variant) =>
66
+ variant.selectedOptions.every(
67
+ (opt) => selected[opt.name] === opt.value,
68
+ ),
69
+ )
70
+ }
71
+
72
+ export function defaultSelectedOptions(
73
+ product: ProductDetail,
74
+ ): Record<string, string> {
75
+ const firstAvailable = product.variants.nodes.find((v) => v.availableForSale)
76
+ const source = firstAvailable ?? product.variants.nodes[0]
77
+ if (!source) return {}
78
+ return Object.fromEntries(source.selectedOptions.map((o) => [o.name, o.value]))
79
+ }
@@ -0,0 +1,276 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import type {
3
+ Image as StorefrontImage,
4
+ MoneyV2,
5
+ } from '@shopify/hydrogen-react/storefront-api-types'
6
+
7
+ import {
8
+ addToCart,
9
+ applyDiscountCode,
10
+ getCart,
11
+ removeCartLine,
12
+ removeDiscountCode,
13
+ updateCartLine,
14
+ } from '#/server/shopify/cart.functions'
15
+ import type { CartDetail, CartLineDetail } from '#/lib/shopify/queries'
16
+
17
+ /**
18
+ * Shared React Query key for the current user's cart.
19
+ *
20
+ * The cart ID lives in an httpOnly cookie on the server, so the client never
21
+ * needs to know it — a single cache key is enough. Route loaders can prefetch
22
+ * into this key so the first render already has the data.
23
+ */
24
+ export const CART_QUERY_KEY = ['shopify', 'cart'] as const
25
+
26
+ const CART_MUTATION_KEY = ['shopify', 'cart', 'mutate'] as const
27
+
28
+ /**
29
+ * Explicit in-flight counter. We don't rely on `queryClient.isMutating()`
30
+ * because its semantics at `onSettled` time vary across React Query versions.
31
+ * A module-level counter is unambiguous: increment in onMutate, decrement in
32
+ * onSettled, invalidate when the count hits zero.
33
+ *
34
+ * @see https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query
35
+ */
36
+ let cartMutationsInFlight = 0
37
+
38
+ function trackMutationStart() {
39
+ cartMutationsInFlight++
40
+ }
41
+
42
+ function settleWhenIdle(qc: ReturnType<typeof useQueryClient>) {
43
+ cartMutationsInFlight = Math.max(0, cartMutationsInFlight - 1)
44
+ if (cartMutationsInFlight === 0) {
45
+ return qc.invalidateQueries({ queryKey: CART_QUERY_KEY })
46
+ }
47
+ }
48
+
49
+ export function useCart() {
50
+ const query = useQuery<CartDetail | null>({
51
+ queryKey: CART_QUERY_KEY,
52
+ queryFn: () => getCart(),
53
+ staleTime: 30_000,
54
+ })
55
+
56
+ return {
57
+ cart: query.data ?? null,
58
+ isLoading: query.isLoading,
59
+ isFetching: query.isFetching,
60
+ isError: query.isError,
61
+ error: query.error,
62
+ refetch: query.refetch,
63
+ totalQuantity: query.data?.totalQuantity ?? 0,
64
+ }
65
+ }
66
+
67
+ type AddToCartLineSnapshot = {
68
+ productTitle: string
69
+ productHandle: string
70
+ variantTitle: string
71
+ price: Pick<MoneyV2, 'amount' | 'currencyCode'>
72
+ image: Pick<StorefrontImage, 'url' | 'altText' | 'width' | 'height'> | null
73
+ selectedOptions: Array<{ name: string; value: string }>
74
+ }
75
+
76
+ type AddToCartInput = {
77
+ variantId: string
78
+ quantity?: number
79
+ /** Product snapshot for optimistic line rendering. */
80
+ line?: AddToCartLineSnapshot
81
+ }
82
+
83
+ export function useAddToCart() {
84
+ const qc = useQueryClient()
85
+
86
+ return useMutation({
87
+ mutationKey: CART_MUTATION_KEY,
88
+ mutationFn: (input: AddToCartInput) =>
89
+ addToCart({
90
+ data: { variantId: input.variantId, quantity: input.quantity ?? 1 },
91
+ }),
92
+
93
+ onMutate: async (input) => {
94
+ trackMutationStart()
95
+ const quantity = input.quantity ?? 1
96
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
97
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
98
+
99
+ if (previous && input.line) {
100
+ const snap = input.line
101
+ const existingIdx = previous.lines.nodes.findIndex(
102
+ (l) => l.merchandise.id === input.variantId,
103
+ )
104
+
105
+ let nextLines: CartDetail['lines']['nodes']
106
+ if (existingIdx >= 0) {
107
+ nextLines = previous.lines.nodes.map((l, i) =>
108
+ i === existingIdx ? { ...l, quantity: l.quantity + quantity } : l,
109
+ )
110
+ } else {
111
+ const lineTotal = String(Number(snap.price.amount) * quantity)
112
+ nextLines = [
113
+ {
114
+ id: `optimistic-${Date.now()}`,
115
+ quantity,
116
+ merchandise: {
117
+ id: input.variantId,
118
+ title: snap.variantTitle,
119
+ availableForSale: true,
120
+ selectedOptions: snap.selectedOptions,
121
+ price: snap.price,
122
+ image: snap.image,
123
+ product: {
124
+ handle: snap.productHandle,
125
+ title: snap.productTitle,
126
+ },
127
+ },
128
+ cost: {
129
+ totalAmount: {
130
+ amount: lineTotal,
131
+ currencyCode: snap.price.currencyCode,
132
+ },
133
+ },
134
+ } satisfies CartLineDetail,
135
+ ...previous.lines.nodes,
136
+ ]
137
+ }
138
+
139
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
140
+ ...previous,
141
+ totalQuantity: nextLines.reduce((s, l) => s + l.quantity, 0),
142
+ lines: { ...previous.lines, nodes: nextLines },
143
+ })
144
+ } else if (previous) {
145
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
146
+ ...previous,
147
+ totalQuantity: (previous.totalQuantity ?? 0) + quantity,
148
+ })
149
+ }
150
+
151
+ return { previous }
152
+ },
153
+
154
+ onError: (_err, _input, ctx) => {
155
+ if (ctx?.previous !== undefined)
156
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
157
+ },
158
+
159
+ onSuccess: (cart) => {
160
+ qc.setQueryData(CART_QUERY_KEY, cart)
161
+ },
162
+
163
+ onSettled: () => settleWhenIdle(qc),
164
+ })
165
+ }
166
+
167
+ export function useUpdateCartLine() {
168
+ const qc = useQueryClient()
169
+ return useMutation({
170
+ mutationKey: CART_MUTATION_KEY,
171
+ mutationFn: (input: { lineId: string; quantity: number }) =>
172
+ updateCartLine({ data: input }),
173
+
174
+ onMutate: async (input) => {
175
+ trackMutationStart()
176
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
177
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
178
+ if (previous) {
179
+ const nextLines = previous.lines.nodes.map((line) =>
180
+ line.id === input.lineId
181
+ ? { ...line, quantity: input.quantity }
182
+ : line,
183
+ )
184
+ const nextQty = nextLines.reduce((sum, line) => sum + line.quantity, 0)
185
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
186
+ ...previous,
187
+ totalQuantity: nextQty,
188
+ lines: { ...previous.lines, nodes: nextLines },
189
+ })
190
+ }
191
+ return { previous }
192
+ },
193
+
194
+ onError: (_err, _input, ctx) => {
195
+ if (ctx?.previous !== undefined)
196
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
197
+ },
198
+
199
+ onSettled: () => settleWhenIdle(qc),
200
+ })
201
+ }
202
+
203
+ export function useRemoveCartLine() {
204
+ const qc = useQueryClient()
205
+ return useMutation({
206
+ mutationKey: CART_MUTATION_KEY,
207
+ mutationFn: (input: { lineId: string }) => removeCartLine({ data: input }),
208
+
209
+ onMutate: async (input) => {
210
+ trackMutationStart()
211
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
212
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
213
+ if (previous) {
214
+ const nextLines = previous.lines.nodes.filter(
215
+ (line) => line.id !== input.lineId,
216
+ )
217
+ const nextQty = nextLines.reduce((sum, line) => sum + line.quantity, 0)
218
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
219
+ ...previous,
220
+ totalQuantity: nextQty,
221
+ lines: { ...previous.lines, nodes: nextLines },
222
+ })
223
+ }
224
+ return { previous }
225
+ },
226
+
227
+ onError: (_err, _input, ctx) => {
228
+ if (ctx?.previous !== undefined)
229
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
230
+ },
231
+
232
+ onSettled: () => settleWhenIdle(qc),
233
+ })
234
+ }
235
+
236
+ export function useApplyDiscountCode() {
237
+ const qc = useQueryClient()
238
+ return useMutation({
239
+ mutationKey: CART_MUTATION_KEY,
240
+ mutationFn: (input: { code: string }) =>
241
+ applyDiscountCode({ data: { code: input.code } }),
242
+ onMutate: async () => {
243
+ trackMutationStart()
244
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
245
+ },
246
+ onSuccess: (cart) => {
247
+ qc.setQueryData(CART_QUERY_KEY, cart)
248
+ },
249
+ onSettled: () => settleWhenIdle(qc),
250
+ })
251
+ }
252
+
253
+ export function useRemoveDiscountCode() {
254
+ const qc = useQueryClient()
255
+ return useMutation({
256
+ mutationKey: CART_MUTATION_KEY,
257
+ mutationFn: () => removeDiscountCode(),
258
+ onMutate: async () => {
259
+ trackMutationStart()
260
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
261
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
262
+ if (previous) {
263
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
264
+ ...previous,
265
+ discountCodes: [],
266
+ })
267
+ }
268
+ return { previous }
269
+ },
270
+ onError: (_err, _input, ctx) => {
271
+ if (ctx?.previous !== undefined)
272
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
273
+ },
274
+ onSettled: () => settleWhenIdle(qc),
275
+ })
276
+ }
@@ -0,0 +1,22 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { useQuery } from '@tanstack/react-query'
3
+
4
+ import { getCustomer } from '#/server/shopify/customer.functions'
5
+ import type { CustomerProfile } from '#/lib/shopify/customer-queries'
6
+
7
+ export const CUSTOMER_QUERY_KEY = ['shopify', 'customer'] as const
8
+
9
+ export function useCustomer() {
10
+ const query = useQuery<CustomerProfile | null>({
11
+ queryKey: CUSTOMER_QUERY_KEY,
12
+ queryFn: () => getCustomer(),
13
+ staleTime: 60_000,
14
+ })
15
+ return {
16
+ customer: query.data ?? null,
17
+ isLoading: query.isLoading,
18
+ isFetching: query.isFetching,
19
+ isError: query.isError,
20
+ error: query.error,
21
+ }
22
+ }
@@ -0,0 +1,37 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ import { useCart } from '#/hooks/use-cart'
4
+
5
+ export default function ShopifyHeaderCart() {
6
+ const { totalQuantity } = useCart()
7
+ return (
8
+ <Link
9
+ to="/shop/cart"
10
+ className="relative inline-flex items-center justify-center rounded-xl p-2 transition hover:bg-[var(--storefront-line)]/40"
11
+ aria-label={`Cart, ${totalQuantity} item${totalQuantity === 1 ? '' : 's'}`}
12
+ >
13
+ <svg
14
+ viewBox="0 0 24 24"
15
+ width="22"
16
+ height="22"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ strokeWidth="1.6"
20
+ aria-hidden="true"
21
+ >
22
+ <path
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ d="M3 4h2.5l1.5 12.5a1.5 1.5 0 0 0 1.5 1.3h9.4a1.5 1.5 0 0 0 1.5-1.2L21 7H6"
26
+ />
27
+ <circle cx="9" cy="20.5" r="1.2" />
28
+ <circle cx="18" cy="20.5" r="1.2" />
29
+ </svg>
30
+ {totalQuantity > 0 && (
31
+ <span className="absolute -right-0.5 -top-0.5 inline-flex min-w-[1.1rem] items-center justify-center rounded-full bg-[var(--storefront-accent,#0a0a0a)] px-1 text-[10px] font-semibold leading-[1.1rem] text-[var(--storefront-accent-fg,#fff)]">
32
+ {totalQuantity}
33
+ </span>
34
+ )}
35
+ </Link>
36
+ )
37
+ }