@tanstack/create 0.65.0 → 0.67.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 (130) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
  3. package/dist/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
  4. package/dist/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
  5. package/dist/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
  6. package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
  7. package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
  8. package/dist/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
  9. package/dist/frameworks/react/add-ons/powersync/info.json +46 -0
  10. package/dist/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
  11. package/dist/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
  12. package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
  13. package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  14. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  15. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  16. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  17. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  18. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  19. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  20. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  21. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  22. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  23. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  24. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  25. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  26. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  27. package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  28. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  29. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  30. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  31. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  32. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  33. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  34. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  35. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  36. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  37. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  38. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  39. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  40. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  41. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  42. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  43. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  44. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  45. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  46. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  47. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  48. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  49. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  50. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  51. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  52. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  53. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  54. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  55. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  56. package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
  57. package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
  58. package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  59. package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
  60. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  61. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  62. package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  63. package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
  64. package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
  65. package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
  66. package/package.json +1 -1
  67. package/src/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
  68. package/src/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
  69. package/src/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
  70. package/src/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
  71. package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
  72. package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
  73. package/src/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
  74. package/src/frameworks/react/add-ons/powersync/info.json +46 -0
  75. package/src/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
  76. package/src/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
  77. package/src/frameworks/react/add-ons/shopify/README.md +86 -0
  78. package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  79. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  80. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  81. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  82. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  83. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  84. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  85. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  86. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  87. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  88. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  89. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  90. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  91. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  92. package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  93. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  94. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  95. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  96. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  97. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  98. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  99. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  100. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  101. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  102. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  103. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  104. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  105. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  106. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  107. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  108. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  109. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  110. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  111. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  112. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  113. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  114. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  115. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  116. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  117. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  118. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  119. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  120. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  121. package/src/frameworks/react/add-ons/shopify/info.json +104 -0
  122. package/src/frameworks/react/add-ons/shopify/package.json +6 -0
  123. package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  124. package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
  125. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  126. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  127. package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  128. package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
  129. package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
  130. package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
@@ -0,0 +1,48 @@
1
+ import { useAddToCart } from '#/hooks/use-cart'
2
+ import type { ProductDetail, ProductDetailVariant } from '#/lib/shopify/queries'
3
+
4
+ type AddToCartButtonProps = {
5
+ product: ProductDetail
6
+ variant: ProductDetailVariant | undefined
7
+ quantity?: number
8
+ }
9
+
10
+ export function AddToCartButton({
11
+ product,
12
+ variant,
13
+ quantity = 1,
14
+ }: AddToCartButtonProps) {
15
+ const { mutate, isPending } = useAddToCart()
16
+ const disabled = !variant || !variant.availableForSale || isPending
17
+
18
+ return (
19
+ <button
20
+ type="button"
21
+ disabled={disabled}
22
+ onClick={() => {
23
+ if (!variant) return
24
+ mutate({
25
+ variantId: variant.id,
26
+ quantity,
27
+ line: {
28
+ productTitle: product.title,
29
+ productHandle: product.handle,
30
+ variantTitle: variant.title,
31
+ price: variant.price,
32
+ image: variant.image ?? product.images.nodes[0] ?? null,
33
+ selectedOptions: variant.selectedOptions,
34
+ },
35
+ })
36
+ }}
37
+ className="w-full rounded-full bg-[var(--storefront-accent)] px-6 py-3.5 text-sm font-medium text-[var(--storefront-accent-fg)] transition disabled:cursor-not-allowed disabled:opacity-50 hover:opacity-90"
38
+ >
39
+ {!variant
40
+ ? 'Select options'
41
+ : !variant.availableForSale
42
+ ? 'Sold out'
43
+ : isPending
44
+ ? 'Adding…'
45
+ : 'Add to cart'}
46
+ </button>
47
+ )
48
+ }
@@ -0,0 +1,94 @@
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 { useRemoveCartLine, useUpdateCartLine } from '#/hooks/use-cart'
6
+ import type { CartLineDetail } from '#/lib/shopify/queries'
7
+
8
+ type CartLineItemProps = {
9
+ line: CartLineDetail
10
+ }
11
+
12
+ export function CartLineItem({ line }: CartLineItemProps) {
13
+ const update = useUpdateCartLine()
14
+ const remove = useRemoveCartLine()
15
+ const merch = line.merchandise
16
+
17
+ return (
18
+ <li className="flex gap-4 border-b border-[var(--storefront-line)] py-6">
19
+ <Link
20
+ to="/shop/products/$handle"
21
+ params={{ handle: merch.product.handle }}
22
+ className="flex-shrink-0"
23
+ >
24
+ <ShopImage
25
+ src={merch.image?.url}
26
+ alt={merch.image?.altText ?? merch.product.title}
27
+ width={120}
28
+ height={150}
29
+ className="h-[150px] w-[120px] rounded-md object-cover"
30
+ />
31
+ </Link>
32
+ <div className="flex flex-1 flex-col gap-2">
33
+ <div className="flex items-start justify-between gap-4">
34
+ <div>
35
+ <Link
36
+ to="/shop/products/$handle"
37
+ params={{ handle: merch.product.handle }}
38
+ className="text-base font-medium no-underline text-[var(--storefront-fg)]"
39
+ >
40
+ {merch.product.title}
41
+ </Link>
42
+ {merch.title && merch.title !== 'Default Title' && (
43
+ <p className="text-sm text-[var(--storefront-fg-muted)]">
44
+ {merch.selectedOptions.map((o) => o.value).join(' · ')}
45
+ </p>
46
+ )}
47
+ </div>
48
+ <Money
49
+ amount={line.cost.totalAmount.amount}
50
+ currencyCode={line.cost.totalAmount.currencyCode}
51
+ className="text-base font-medium"
52
+ />
53
+ </div>
54
+ <div className="mt-auto flex items-center justify-between gap-4">
55
+ <div className="inline-flex items-center rounded-full border border-[var(--storefront-line)]">
56
+ <button
57
+ type="button"
58
+ aria-label="Decrease quantity"
59
+ disabled={update.isPending || line.quantity <= 1}
60
+ onClick={() =>
61
+ update.mutate({ lineId: line.id, quantity: line.quantity - 1 })
62
+ }
63
+ className="px-3 py-1.5 text-base disabled:opacity-40"
64
+ >
65
+
66
+ </button>
67
+ <span className="min-w-[2rem] text-center text-sm">
68
+ {line.quantity}
69
+ </span>
70
+ <button
71
+ type="button"
72
+ aria-label="Increase quantity"
73
+ disabled={update.isPending}
74
+ onClick={() =>
75
+ update.mutate({ lineId: line.id, quantity: line.quantity + 1 })
76
+ }
77
+ className="px-3 py-1.5 text-base disabled:opacity-40"
78
+ >
79
+ +
80
+ </button>
81
+ </div>
82
+ <button
83
+ type="button"
84
+ onClick={() => remove.mutate({ lineId: line.id })}
85
+ disabled={remove.isPending}
86
+ className="text-sm text-[var(--storefront-fg-muted)] underline underline-offset-4 hover:text-[var(--storefront-fg)] disabled:opacity-40"
87
+ >
88
+ Remove
89
+ </button>
90
+ </div>
91
+ </div>
92
+ </li>
93
+ )
94
+ }
@@ -0,0 +1,111 @@
1
+ import { useState } from 'react'
2
+
3
+ import { Money } from '#/components/shop/money'
4
+ import {
5
+ useApplyDiscountCode,
6
+ useRemoveDiscountCode,
7
+ } from '#/hooks/use-cart'
8
+ import type { CartDetail } from '#/lib/shopify/queries'
9
+
10
+ type CartSummaryProps = {
11
+ cart: CartDetail
12
+ }
13
+
14
+ export function CartSummary({ cart }: CartSummaryProps) {
15
+ const [code, setCode] = useState('')
16
+ const apply = useApplyDiscountCode()
17
+ const remove = useRemoveDiscountCode()
18
+ const appliedCode = cart.discountCodes.find((c) => c.applicable)
19
+
20
+ return (
21
+ <aside className="flex flex-col gap-4 rounded-2xl border border-[var(--storefront-line)] p-6">
22
+ <h2 className="text-lg font-medium">Order summary</h2>
23
+
24
+ <div className="flex justify-between text-sm">
25
+ <span>Subtotal</span>
26
+ <Money
27
+ amount={cart.cost.subtotalAmount.amount}
28
+ currencyCode={cart.cost.subtotalAmount.currencyCode}
29
+ />
30
+ </div>
31
+ {cart.cost.totalTaxAmount && Number(cart.cost.totalTaxAmount.amount) > 0 && (
32
+ <div className="flex justify-between text-sm text-[var(--storefront-fg-muted)]">
33
+ <span>Estimated tax</span>
34
+ <Money
35
+ amount={cart.cost.totalTaxAmount.amount}
36
+ currencyCode={cart.cost.totalTaxAmount.currencyCode}
37
+ />
38
+ </div>
39
+ )}
40
+ <p className="text-sm text-[var(--storefront-fg-muted)]">
41
+ Shipping calculated at checkout.
42
+ </p>
43
+
44
+ <form
45
+ onSubmit={(e) => {
46
+ e.preventDefault()
47
+ if (code.trim()) {
48
+ apply.mutate(
49
+ { code: code.trim() },
50
+ { onSuccess: () => setCode('') },
51
+ )
52
+ }
53
+ }}
54
+ className="flex flex-col gap-2"
55
+ >
56
+ <div className="flex gap-2">
57
+ <input
58
+ type="text"
59
+ value={code}
60
+ onChange={(e) => setCode(e.target.value)}
61
+ placeholder="Discount code"
62
+ className="min-w-0 flex-1 rounded-full border border-[var(--storefront-line)] bg-transparent px-4 py-2 text-sm focus:border-[var(--storefront-accent)] focus:outline-none"
63
+ />
64
+ <button
65
+ type="submit"
66
+ disabled={apply.isPending || !code.trim()}
67
+ className="rounded-full border border-[var(--storefront-line)] px-4 py-2 text-sm font-medium hover:border-[var(--storefront-accent)] disabled:opacity-40"
68
+ >
69
+ Apply
70
+ </button>
71
+ </div>
72
+ {apply.error && (
73
+ <p className="text-xs text-red-600">{(apply.error as Error).message}</p>
74
+ )}
75
+ {appliedCode && (
76
+ <div className="flex items-center justify-between rounded-full bg-[var(--storefront-line)]/40 px-4 py-2 text-xs">
77
+ <span>
78
+ Applied: <strong>{appliedCode.code}</strong>
79
+ </span>
80
+ <button
81
+ type="button"
82
+ onClick={() => remove.mutate()}
83
+ disabled={remove.isPending}
84
+ className="underline underline-offset-2 disabled:opacity-40"
85
+ >
86
+ Remove
87
+ </button>
88
+ </div>
89
+ )}
90
+ </form>
91
+
92
+ <div className="flex justify-between border-t border-[var(--storefront-line)] pt-4 text-base font-medium">
93
+ <span>Total</span>
94
+ <Money
95
+ amount={cart.cost.totalAmount.amount}
96
+ currencyCode={cart.cost.totalAmount.currencyCode}
97
+ />
98
+ </div>
99
+
100
+ <a
101
+ href={cart.checkoutUrl}
102
+ className="block w-full rounded-full bg-[var(--storefront-accent)] px-6 py-3.5 text-center text-sm font-medium text-[var(--storefront-accent-fg)] no-underline transition hover:opacity-90"
103
+ >
104
+ Checkout
105
+ </a>
106
+ <p className="text-center text-xs text-[var(--storefront-fg-muted)]">
107
+ Secure checkout powered by Shopify
108
+ </p>
109
+ </aside>
110
+ )
111
+ }
@@ -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
+ }